hookdeck-cli 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish-npm.yml +19 -0
- package/.github/workflows/release.yml +85 -0
- package/.goreleaser/linux.yml +68 -0
- package/.goreleaser/mac.yml +69 -0
- package/.goreleaser/windows.yml +52 -0
- package/.tool-versions +1 -0
- package/Dockerfile +5 -0
- package/LICENSE +202 -0
- package/README.md +223 -0
- package/docs/cli-demo.gif +0 -0
- package/go.mod +58 -0
- package/go.sum +444 -0
- package/main.go +22 -0
- package/package.json +30 -0
- package/pkg/ansi/ansi.go +208 -0
- package/pkg/ansi/init_windows.go +23 -0
- package/pkg/cmd/completion.go +128 -0
- package/pkg/cmd/listen.go +114 -0
- package/pkg/cmd/login.go +37 -0
- package/pkg/cmd/logout.go +35 -0
- package/pkg/cmd/root.go +112 -0
- package/pkg/cmd/version.go +25 -0
- package/pkg/cmd/whoami.go +50 -0
- package/pkg/config/config.go +326 -0
- package/pkg/config/config_test.go +20 -0
- package/pkg/config/profile.go +296 -0
- package/pkg/config/profile_test.go +109 -0
- package/pkg/hookdeck/client.go +210 -0
- package/pkg/hookdeck/client_test.go +203 -0
- package/pkg/hookdeck/connections.go +61 -0
- package/pkg/hookdeck/destinations.go +14 -0
- package/pkg/hookdeck/guest.go +37 -0
- package/pkg/hookdeck/session.go +37 -0
- package/pkg/hookdeck/sources.go +73 -0
- package/pkg/hookdeck/telemetry.go +84 -0
- package/pkg/hookdeck/telemetry_test.go +35 -0
- package/pkg/hookdeck/verbosetransport.go +82 -0
- package/pkg/hookdeck/verbosetransport_test.go +47 -0
- package/pkg/listen/connection.go +91 -0
- package/pkg/listen/listen.go +140 -0
- package/pkg/listen/source.go +84 -0
- package/pkg/login/client_login.go +209 -0
- package/pkg/login/interactive_login.go +180 -0
- package/pkg/login/login_message.go +24 -0
- package/pkg/login/poll.go +78 -0
- package/pkg/login/validate.go +52 -0
- package/pkg/logout/logout.go +48 -0
- package/pkg/open/open.go +50 -0
- package/pkg/proxy/proxy.go +433 -0
- package/pkg/useragent/uname_unix.go +25 -0
- package/pkg/useragent/uname_unix_test.go +24 -0
- package/pkg/useragent/uname_windows.go +9 -0
- package/pkg/useragent/useragent.go +74 -0
- package/pkg/validators/cmds.go +71 -0
- package/pkg/validators/validate.go +144 -0
- package/pkg/version/version.go +58 -0
- package/pkg/version/version_test.go +15 -0
- package/pkg/websocket/attempt_messages.go +47 -0
- package/pkg/websocket/client.go +525 -0
- package/pkg/websocket/connection_messages.go +11 -0
- package/pkg/websocket/messages.go +64 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
package login
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"io/ioutil"
|
|
9
|
+
"net/http"
|
|
10
|
+
"net/url"
|
|
11
|
+
"os"
|
|
12
|
+
|
|
13
|
+
"github.com/briandowns/spinner"
|
|
14
|
+
|
|
15
|
+
"github.com/hookdeck/hookdeck-cli/pkg/ansi"
|
|
16
|
+
"github.com/hookdeck/hookdeck-cli/pkg/config"
|
|
17
|
+
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
|
|
18
|
+
"github.com/hookdeck/hookdeck-cli/pkg/open"
|
|
19
|
+
"github.com/hookdeck/hookdeck-cli/pkg/validators"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
var openBrowser = open.Browser
|
|
23
|
+
var canOpenBrowser = open.CanOpenBrowser
|
|
24
|
+
|
|
25
|
+
const hookdeckCLIAuthPath = "/cli-auth"
|
|
26
|
+
|
|
27
|
+
// Links provides the URLs for the CLI to continue the login flow
|
|
28
|
+
type Links struct {
|
|
29
|
+
BrowserURL string `json:"browser_url"`
|
|
30
|
+
PollURL string `json:"poll_url"`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Login function is used to obtain credentials via hookdeck dashboard.
|
|
34
|
+
func Login(config *config.Config, input io.Reader) error {
|
|
35
|
+
var s *spinner.Spinner
|
|
36
|
+
|
|
37
|
+
if config.Profile.APIKey != "" {
|
|
38
|
+
s = ansi.StartNewSpinner("Verifying CLI Key...", os.Stdout)
|
|
39
|
+
response, err := ValidateKey(config.APIBaseURL, config.Profile.APIKey)
|
|
40
|
+
if err != nil {
|
|
41
|
+
return err
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
config.Profile.ClientID = response.ClientID
|
|
45
|
+
config.Profile.DisplayName = response.UserName
|
|
46
|
+
config.Profile.TeamName = response.TeamName
|
|
47
|
+
config.Profile.TeamMode = response.TeamMode
|
|
48
|
+
config.Profile.TeamID = response.TeamID
|
|
49
|
+
|
|
50
|
+
profileErr := config.Profile.CreateProfile()
|
|
51
|
+
if profileErr != nil {
|
|
52
|
+
return profileErr
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
message := SuccessMessage(response.UserName, response.TeamName, config.Profile.TeamMode == "console")
|
|
56
|
+
ansi.StopSpinner(s, message, os.Stdout)
|
|
57
|
+
|
|
58
|
+
return nil
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
links, err := getLinks(config.APIBaseURL, config.Profile.DeviceName)
|
|
62
|
+
if err != nil {
|
|
63
|
+
return err
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if isSSH() || !canOpenBrowser() {
|
|
67
|
+
fmt.Printf("To authenticate with Hookdeck, please go to: %s\n", links.BrowserURL)
|
|
68
|
+
|
|
69
|
+
s = ansi.StartNewSpinner("Waiting for confirmation...", os.Stdout)
|
|
70
|
+
} else {
|
|
71
|
+
fmt.Printf("Press Enter to open the browser (^C to quit)")
|
|
72
|
+
fmt.Fscanln(input)
|
|
73
|
+
|
|
74
|
+
s = ansi.StartNewSpinner("Waiting for confirmation...", os.Stdout)
|
|
75
|
+
|
|
76
|
+
err = openBrowser(links.BrowserURL)
|
|
77
|
+
if err != nil {
|
|
78
|
+
msg := fmt.Sprintf("Failed to open browser, please go to %s manually.", links.BrowserURL)
|
|
79
|
+
ansi.StopSpinner(s, msg, os.Stdout)
|
|
80
|
+
s = ansi.StartNewSpinner("Waiting for confirmation...", os.Stdout)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Call poll function
|
|
85
|
+
response, err := PollForKey(links.PollURL, 0, 0)
|
|
86
|
+
if err != nil {
|
|
87
|
+
return err
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
validateErr := validators.APIKey(response.APIKey)
|
|
91
|
+
if validateErr != nil {
|
|
92
|
+
return validateErr
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
config.Profile.APIKey = response.APIKey
|
|
96
|
+
config.Profile.ClientID = response.ClientID
|
|
97
|
+
config.Profile.DisplayName = response.UserName
|
|
98
|
+
config.Profile.TeamName = response.TeamName
|
|
99
|
+
config.Profile.TeamMode = response.TeamMode
|
|
100
|
+
config.Profile.TeamID = response.TeamID
|
|
101
|
+
|
|
102
|
+
profileErr := config.Profile.CreateProfile()
|
|
103
|
+
if profileErr != nil {
|
|
104
|
+
return profileErr
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
message := SuccessMessage(response.UserName, response.TeamName, response.TeamMode == "console")
|
|
108
|
+
ansi.StopSpinner(s, message, os.Stdout)
|
|
109
|
+
|
|
110
|
+
return nil
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func GuestLogin(config *config.Config) (string, error) {
|
|
114
|
+
parsedBaseURL, err := url.Parse(config.APIBaseURL)
|
|
115
|
+
if err != nil {
|
|
116
|
+
return "", err
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
client := &hookdeck.Client{
|
|
120
|
+
BaseURL: parsedBaseURL,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fmt.Println("🚩 Not connected with any account. Creating a guest account...")
|
|
124
|
+
|
|
125
|
+
guest_user, err := client.CreateGuestUser(hookdeck.CreateGuestUserInput{
|
|
126
|
+
DeviceName: config.Profile.DeviceName,
|
|
127
|
+
})
|
|
128
|
+
if err != nil {
|
|
129
|
+
return "", err
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Call poll function
|
|
133
|
+
response, err := PollForKey(guest_user.PollURL, 0, 0)
|
|
134
|
+
if err != nil {
|
|
135
|
+
return "", err
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
validateErr := validators.APIKey(response.APIKey)
|
|
139
|
+
if validateErr != nil {
|
|
140
|
+
return "", validateErr
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
config.Profile.APIKey = response.APIKey
|
|
144
|
+
config.Profile.ClientID = response.ClientID
|
|
145
|
+
config.Profile.DisplayName = response.UserName
|
|
146
|
+
config.Profile.TeamName = response.TeamName
|
|
147
|
+
config.Profile.TeamMode = response.TeamMode
|
|
148
|
+
config.Profile.TeamID = response.TeamID
|
|
149
|
+
|
|
150
|
+
profileErr := config.Profile.CreateProfile()
|
|
151
|
+
if profileErr != nil {
|
|
152
|
+
return "", profileErr
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return guest_user.Url, nil
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func getLinks(baseURL string, deviceName string) (*Links, error) {
|
|
159
|
+
parsedBaseURL, err := url.Parse(baseURL)
|
|
160
|
+
if err != nil {
|
|
161
|
+
return nil, err
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
client := &hookdeck.Client{
|
|
165
|
+
BaseURL: parsedBaseURL,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
data := struct {
|
|
169
|
+
DeviceName string `json:"device_name"`
|
|
170
|
+
}{}
|
|
171
|
+
data.DeviceName = deviceName
|
|
172
|
+
json_data, err := json.Marshal(data)
|
|
173
|
+
if err != nil {
|
|
174
|
+
return nil, err
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
res, err := client.Post(context.TODO(), hookdeckCLIAuthPath, json_data, nil)
|
|
178
|
+
if err != nil {
|
|
179
|
+
return nil, err
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
defer res.Body.Close()
|
|
183
|
+
|
|
184
|
+
bodyBytes, err := ioutil.ReadAll(res.Body)
|
|
185
|
+
if err != nil {
|
|
186
|
+
return nil, err
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if res.StatusCode != http.StatusOK {
|
|
190
|
+
return nil, fmt.Errorf("unexpected http status code: %d %s", res.StatusCode, string(bodyBytes))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
var links Links
|
|
194
|
+
|
|
195
|
+
err = json.Unmarshal(bodyBytes, &links)
|
|
196
|
+
if err != nil {
|
|
197
|
+
return nil, err
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return &links, nil
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func isSSH() bool {
|
|
204
|
+
if os.Getenv("SSH_TTY") != "" || os.Getenv("SSH_CONNECTION") != "" || os.Getenv("SSH_CLIENT") != "" {
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return false
|
|
209
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
package login
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"errors"
|
|
8
|
+
"fmt"
|
|
9
|
+
"io"
|
|
10
|
+
"net/url"
|
|
11
|
+
"os"
|
|
12
|
+
"os/signal"
|
|
13
|
+
"strings"
|
|
14
|
+
"syscall"
|
|
15
|
+
|
|
16
|
+
"golang.org/x/term"
|
|
17
|
+
|
|
18
|
+
"github.com/hookdeck/hookdeck-cli/pkg/ansi"
|
|
19
|
+
"github.com/hookdeck/hookdeck-cli/pkg/config"
|
|
20
|
+
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
|
|
21
|
+
"github.com/hookdeck/hookdeck-cli/pkg/validators"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// InteractiveLogin lets the user set configuration on the command line
|
|
25
|
+
func InteractiveLogin(config *config.Config) error {
|
|
26
|
+
apiKey, err := getConfigureAPIKey(os.Stdin)
|
|
27
|
+
if err != nil {
|
|
28
|
+
return err
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
config.Profile.DeviceName = getConfigureDeviceName(os.Stdin)
|
|
32
|
+
|
|
33
|
+
s := ansi.StartNewSpinner("Waiting for confirmation...", os.Stdout)
|
|
34
|
+
|
|
35
|
+
// Call poll function
|
|
36
|
+
response, err := PollForKey(config.APIBaseURL+"/cli-auth/poll?key="+apiKey, 0, 0)
|
|
37
|
+
if err != nil {
|
|
38
|
+
return err
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
parsedBaseURL, err := url.Parse(config.APIBaseURL)
|
|
42
|
+
if err != nil {
|
|
43
|
+
return err
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
client := &hookdeck.Client{
|
|
47
|
+
BaseURL: parsedBaseURL,
|
|
48
|
+
APIKey: response.APIKey,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
data := struct {
|
|
52
|
+
DeviceName string `json:"device_name"`
|
|
53
|
+
}{}
|
|
54
|
+
data.DeviceName = config.Profile.DeviceName
|
|
55
|
+
json_data, err := json.Marshal(data)
|
|
56
|
+
if err != nil {
|
|
57
|
+
return err
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_, err = client.Put(context.TODO(), "/cli/"+response.ClientID, json_data, nil)
|
|
61
|
+
if err != nil {
|
|
62
|
+
return err
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
config.Profile.APIKey = response.APIKey
|
|
66
|
+
config.Profile.ClientID = response.ClientID
|
|
67
|
+
config.Profile.DisplayName = response.UserName
|
|
68
|
+
config.Profile.TeamName = response.TeamName
|
|
69
|
+
config.Profile.TeamMode = response.TeamMode
|
|
70
|
+
config.Profile.TeamID = response.TeamID
|
|
71
|
+
|
|
72
|
+
profileErr := config.Profile.CreateProfile()
|
|
73
|
+
if profileErr != nil {
|
|
74
|
+
ansi.StopSpinner(s, "", os.Stdout)
|
|
75
|
+
return profileErr
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
message := SuccessMessage(response.UserName, response.TeamName, response.TeamMode == "console")
|
|
79
|
+
|
|
80
|
+
ansi.StopSpinner(s, message, os.Stdout)
|
|
81
|
+
|
|
82
|
+
return nil
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func getConfigureAPIKey(input io.Reader) (string, error) {
|
|
86
|
+
fmt.Print("Enter your CLI API key: ")
|
|
87
|
+
|
|
88
|
+
apiKey, err := securePrompt(input)
|
|
89
|
+
if err != nil {
|
|
90
|
+
return "", err
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
apiKey = strings.TrimSpace(apiKey)
|
|
94
|
+
if apiKey == "" {
|
|
95
|
+
return "", errors.New("CLI API key is required, please provide your CLI API key")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
err = validators.APIKey(apiKey)
|
|
99
|
+
if err != nil {
|
|
100
|
+
return "", err
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fmt.Printf("Your API key is: %s\n", redactAPIKey(apiKey))
|
|
104
|
+
|
|
105
|
+
return apiKey, nil
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func getConfigureDeviceName(input io.Reader) string {
|
|
109
|
+
hostName, _ := os.Hostname()
|
|
110
|
+
reader := bufio.NewReader(input)
|
|
111
|
+
|
|
112
|
+
color := ansi.Color(os.Stdout)
|
|
113
|
+
fmt.Printf("How would you like to identify this device in the Hookdeck Dashboard? [default: %s] ", color.Bold(color.Cyan(hostName)))
|
|
114
|
+
|
|
115
|
+
deviceName, _ := reader.ReadString('\n')
|
|
116
|
+
if strings.TrimSpace(deviceName) == "" {
|
|
117
|
+
deviceName = hostName
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return deviceName
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// redactAPIKey returns a redacted version of API keys. The first 8 and last 4
|
|
124
|
+
// characters are not redacted, everything else is replaced by "*" characters.
|
|
125
|
+
//
|
|
126
|
+
// It panics if the provided string has less than 12 characters.
|
|
127
|
+
func redactAPIKey(apiKey string) string {
|
|
128
|
+
var b strings.Builder
|
|
129
|
+
|
|
130
|
+
b.WriteString(apiKey[0:8]) // #nosec G104 (gosec bug: https://github.com/securego/gosec/issues/267)
|
|
131
|
+
b.WriteString(strings.Repeat("*", len(apiKey)-12)) // #nosec G104 (gosec bug: https://github.com/securego/gosec/issues/267)
|
|
132
|
+
b.WriteString(apiKey[len(apiKey)-4:]) // #nosec G104 (gosec bug: https://github.com/securego/gosec/issues/267)
|
|
133
|
+
|
|
134
|
+
return b.String()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func securePrompt(input io.Reader) (string, error) {
|
|
138
|
+
if input == os.Stdin {
|
|
139
|
+
// terminal.ReadPassword does not reset terminal state on ctrl-c interrupts,
|
|
140
|
+
// this results in the terminal input staying hidden after program exit.
|
|
141
|
+
// We need to manually catch the interrupt and restore terminal state before exiting.
|
|
142
|
+
signalChan, err := protectTerminalState()
|
|
143
|
+
if err != nil {
|
|
144
|
+
return "", err
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
buf, err := term.ReadPassword(int(syscall.Stdin)) //nolint:unconvert
|
|
148
|
+
if err != nil {
|
|
149
|
+
return "", err
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
signal.Stop(signalChan)
|
|
153
|
+
|
|
154
|
+
fmt.Print("\n")
|
|
155
|
+
|
|
156
|
+
return string(buf), nil
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
reader := bufio.NewReader(input)
|
|
160
|
+
|
|
161
|
+
return reader.ReadString('\n')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func protectTerminalState() (chan os.Signal, error) {
|
|
165
|
+
originalTerminalState, err := term.GetState(int(syscall.Stdin)) //nolint:unconvert
|
|
166
|
+
if err != nil {
|
|
167
|
+
return nil, err
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
signalChan := make(chan os.Signal)
|
|
171
|
+
signal.Notify(signalChan, os.Interrupt)
|
|
172
|
+
|
|
173
|
+
go func() {
|
|
174
|
+
<-signalChan
|
|
175
|
+
term.Restore(int(syscall.Stdin), originalTerminalState) //nolint:unconvert
|
|
176
|
+
os.Exit(1)
|
|
177
|
+
}()
|
|
178
|
+
|
|
179
|
+
return signalChan, nil
|
|
180
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package login
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
|
|
7
|
+
"github.com/hookdeck/hookdeck-cli/pkg/ansi"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// SuccessMessage returns the display message for a successfully authenticated user
|
|
11
|
+
func SuccessMessage(displayName string, teamName string, isConsole bool) string {
|
|
12
|
+
color := ansi.Color(os.Stdout)
|
|
13
|
+
|
|
14
|
+
if isConsole == true {
|
|
15
|
+
return fmt.Sprintf(
|
|
16
|
+
"Done! The Hookdeck CLI is configured with your console Sandbox",
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
return fmt.Sprintf(
|
|
20
|
+
"Done! The Hookdeck CLI is configured for %s in workspace %s\n",
|
|
21
|
+
color.Bold(displayName),
|
|
22
|
+
color.Bold(teamName),
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
package login
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"io/ioutil"
|
|
8
|
+
"net/url"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const maxAttemptsDefault = 2 * 60
|
|
15
|
+
const intervalDefault = 1 * time.Second
|
|
16
|
+
|
|
17
|
+
// PollAPIKeyResponse returns the data of the polling client login
|
|
18
|
+
type PollAPIKeyResponse struct {
|
|
19
|
+
Claimed bool `json:"claimed"`
|
|
20
|
+
UserID string `json:"user_id"`
|
|
21
|
+
UserName string `json:"user_name"`
|
|
22
|
+
TeamID string `json:"team_id"`
|
|
23
|
+
TeamName string `json:"team_name"`
|
|
24
|
+
TeamMode string `json:"team_mode"`
|
|
25
|
+
APIKey string `json:"key"`
|
|
26
|
+
ClientID string `json:"client_id"`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// PollForKey polls Hookdeck at the specified interval until either the API key is available or we've reached the max attempts.
|
|
30
|
+
func PollForKey(pollURL string, interval time.Duration, maxAttempts int) (*PollAPIKeyResponse, error) {
|
|
31
|
+
if maxAttempts == 0 {
|
|
32
|
+
maxAttempts = maxAttemptsDefault
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if interval == 0 {
|
|
36
|
+
interval = intervalDefault
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
parsedURL, err := url.Parse(pollURL)
|
|
40
|
+
if err != nil {
|
|
41
|
+
return nil, err
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
baseURL := &url.URL{Scheme: parsedURL.Scheme, Host: parsedURL.Host}
|
|
45
|
+
|
|
46
|
+
client := &hookdeck.Client{
|
|
47
|
+
BaseURL: baseURL,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
var count = 0
|
|
51
|
+
for count < maxAttempts {
|
|
52
|
+
res, err := client.Get(context.TODO(), parsedURL.Path, parsedURL.Query().Encode(), nil)
|
|
53
|
+
if err != nil {
|
|
54
|
+
return nil, err
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
var response PollAPIKeyResponse
|
|
58
|
+
|
|
59
|
+
defer res.Body.Close()
|
|
60
|
+
body, err := ioutil.ReadAll(res.Body)
|
|
61
|
+
if err != nil {
|
|
62
|
+
return nil, err
|
|
63
|
+
}
|
|
64
|
+
err = json.Unmarshal(body, &response)
|
|
65
|
+
if err != nil {
|
|
66
|
+
return nil, err
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if response.Claimed {
|
|
70
|
+
return &response, nil
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
count++
|
|
74
|
+
time.Sleep(interval)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return nil, errors.New("exceeded max attempts")
|
|
78
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package login
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"io/ioutil"
|
|
7
|
+
"net/url"
|
|
8
|
+
|
|
9
|
+
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// ValidateAPIKeyResponse returns the user and team associated with a key
|
|
13
|
+
type ValidateAPIKeyResponse struct {
|
|
14
|
+
UserID string `json:"user_id"`
|
|
15
|
+
UserName string `json:"user_name"`
|
|
16
|
+
TeamID string `json:"team_id"`
|
|
17
|
+
TeamName string `json:"team_name"`
|
|
18
|
+
TeamMode string `json:"team_mode"`
|
|
19
|
+
ClientID string `json:"client_id"`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func ValidateKey(baseURL string, key string) (*ValidateAPIKeyResponse, error) {
|
|
23
|
+
|
|
24
|
+
parsedBaseURL, err := url.Parse(baseURL)
|
|
25
|
+
if err != nil {
|
|
26
|
+
return nil, err
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
client := &hookdeck.Client{
|
|
30
|
+
BaseURL: parsedBaseURL,
|
|
31
|
+
APIKey: key,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
res, err := client.Get(context.Background(), "/cli-auth/validate", "", nil)
|
|
35
|
+
if err != nil {
|
|
36
|
+
return nil, err
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
var response ValidateAPIKeyResponse
|
|
40
|
+
|
|
41
|
+
defer res.Body.Close()
|
|
42
|
+
body, err := ioutil.ReadAll(res.Body)
|
|
43
|
+
if err != nil {
|
|
44
|
+
return nil, err
|
|
45
|
+
}
|
|
46
|
+
err = json.Unmarshal(body, &response)
|
|
47
|
+
if err != nil {
|
|
48
|
+
return nil, err
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return &response, nil
|
|
52
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package logout
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
|
|
6
|
+
"github.com/hookdeck/hookdeck-cli/pkg/config"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// Logout function is used to clear the credentials set for the current Profile
|
|
10
|
+
func Logout(config *config.Config) error {
|
|
11
|
+
key, _ := config.Profile.GetAPIKey()
|
|
12
|
+
|
|
13
|
+
if key == "" {
|
|
14
|
+
fmt.Println("You are already logged out.")
|
|
15
|
+
return nil
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fmt.Println("Logging out...")
|
|
19
|
+
|
|
20
|
+
profileName := config.Profile.ProfileName
|
|
21
|
+
|
|
22
|
+
err := config.RemoveProfile(profileName)
|
|
23
|
+
if err != nil {
|
|
24
|
+
return err
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if profileName == "default" {
|
|
28
|
+
fmt.Println("Credentials have been cleared for the default project.")
|
|
29
|
+
} else {
|
|
30
|
+
fmt.Printf("Credentials have been cleared for %s.\n", profileName)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return nil
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// All function is used to clear the credentials on all profiles
|
|
37
|
+
func All(cfg *config.Config) error {
|
|
38
|
+
fmt.Println("Logging out...")
|
|
39
|
+
|
|
40
|
+
err := cfg.RemoveAllProfiles()
|
|
41
|
+
if err != nil {
|
|
42
|
+
return err
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fmt.Println("Credentials have been cleared for all projects.")
|
|
46
|
+
|
|
47
|
+
return nil
|
|
48
|
+
}
|
package/pkg/open/open.go
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package open
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os/exec"
|
|
6
|
+
"runtime"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
var execCommand = exec.Command
|
|
10
|
+
|
|
11
|
+
// Browser takes a url and opens it using the default browser on the operating system
|
|
12
|
+
func Browser(url string) error {
|
|
13
|
+
var err error
|
|
14
|
+
|
|
15
|
+
switch runtime.GOOS {
|
|
16
|
+
case "linux":
|
|
17
|
+
err = execCommand("xdg-open", url).Start()
|
|
18
|
+
case "windows":
|
|
19
|
+
err = execCommand("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
|
20
|
+
case "darwin":
|
|
21
|
+
err = execCommand("open", url).Start()
|
|
22
|
+
default:
|
|
23
|
+
err = fmt.Errorf("unsupported platform")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if err != nil {
|
|
27
|
+
return err
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// CanOpenBrowser determines if no browser is set in linux
|
|
34
|
+
func CanOpenBrowser() bool {
|
|
35
|
+
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
output, err := execCommand("xdg-settings", "get", "default-web-browser").Output()
|
|
40
|
+
|
|
41
|
+
if err != nil {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if string(output) == "" {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return true
|
|
50
|
+
}
|