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,73 @@
|
|
|
1
|
+
package hookdeck
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"net/http"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type Source struct {
|
|
11
|
+
Id string
|
|
12
|
+
Alias string
|
|
13
|
+
Label string
|
|
14
|
+
Url string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type CreateSourceInput struct {
|
|
18
|
+
Alias string `json:"alias"`
|
|
19
|
+
Label string `json:"label"`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type SourceList struct {
|
|
23
|
+
Count int
|
|
24
|
+
Models []Source
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func (c *Client) GetSourceByAlias(alias string) (Source, error) {
|
|
28
|
+
res, err := c.Get(context.Background(), "/sources", "alias="+alias, nil)
|
|
29
|
+
if err != nil {
|
|
30
|
+
return Source{}, err
|
|
31
|
+
}
|
|
32
|
+
if res.StatusCode != http.StatusOK {
|
|
33
|
+
return Source{}, fmt.Errorf("unexpected http status code: %d %s", res.StatusCode, err)
|
|
34
|
+
}
|
|
35
|
+
sources := SourceList{}
|
|
36
|
+
postprocessJsonResponse(res, &sources)
|
|
37
|
+
|
|
38
|
+
if len(sources.Models) > 0 {
|
|
39
|
+
return sources.Models[0], nil
|
|
40
|
+
}
|
|
41
|
+
return Source{}, nil
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func (c *Client) ListSources() ([]Source, error) {
|
|
45
|
+
res, err := c.Get(context.Background(), "/sources", "", nil)
|
|
46
|
+
if err != nil {
|
|
47
|
+
return []Source{}, err
|
|
48
|
+
}
|
|
49
|
+
if res.StatusCode != http.StatusOK {
|
|
50
|
+
return []Source{}, fmt.Errorf("unexpected http status code: %d %s", res.StatusCode, err)
|
|
51
|
+
}
|
|
52
|
+
sources := SourceList{}
|
|
53
|
+
postprocessJsonResponse(res, &sources)
|
|
54
|
+
|
|
55
|
+
return sources.Models, nil
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func (c *Client) CreateSource(input CreateSourceInput) (Source, error) {
|
|
59
|
+
input_bytes, err := json.Marshal(input)
|
|
60
|
+
if err != nil {
|
|
61
|
+
return Source{}, err
|
|
62
|
+
}
|
|
63
|
+
res, err := c.Post(context.Background(), "/sources", input_bytes, nil)
|
|
64
|
+
if err != nil {
|
|
65
|
+
return Source{}, err
|
|
66
|
+
}
|
|
67
|
+
if res.StatusCode != http.StatusOK {
|
|
68
|
+
return Source{}, fmt.Errorf("unexpected http status code: %d %s", res.StatusCode, err)
|
|
69
|
+
}
|
|
70
|
+
source := Source{}
|
|
71
|
+
postprocessJsonResponse(res, &source)
|
|
72
|
+
return source, nil
|
|
73
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package hookdeck
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"strings"
|
|
6
|
+
"sync"
|
|
7
|
+
|
|
8
|
+
"github.com/spf13/cobra"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
//
|
|
12
|
+
// Public types
|
|
13
|
+
//
|
|
14
|
+
|
|
15
|
+
// CLITelemetry is the structure that holds telemetry data sent to Hookdeck in
|
|
16
|
+
// API requests.
|
|
17
|
+
type CLITelemetry struct {
|
|
18
|
+
CommandPath string `json:"command_path"`
|
|
19
|
+
DeviceName string `json:"device_name"`
|
|
20
|
+
GeneratedResource bool `json:"generated_resource"`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// SetCommandContext sets the telemetry values for the command being executed.
|
|
24
|
+
func (t *CLITelemetry) SetCommandContext(cmd *cobra.Command) {
|
|
25
|
+
t.CommandPath = cmd.CommandPath()
|
|
26
|
+
t.GeneratedResource = false
|
|
27
|
+
|
|
28
|
+
for _, value := range cmd.Annotations {
|
|
29
|
+
// Generated commands have an annotation called "operation", we can
|
|
30
|
+
// search for that to let us know it's generated
|
|
31
|
+
if value == "operation" {
|
|
32
|
+
t.GeneratedResource = true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// SetDeviceName puts the device name into telemetry
|
|
38
|
+
func (t *CLITelemetry) SetDeviceName(deviceName string) {
|
|
39
|
+
t.DeviceName = deviceName
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//
|
|
43
|
+
// Public functions
|
|
44
|
+
//
|
|
45
|
+
|
|
46
|
+
// GetTelemetryInstance returns the CLITelemetry instance (initializing it
|
|
47
|
+
// first if necessary).
|
|
48
|
+
func GetTelemetryInstance() *CLITelemetry {
|
|
49
|
+
once.Do(func() {
|
|
50
|
+
instance = &CLITelemetry{}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return instance
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//
|
|
57
|
+
// Private variables
|
|
58
|
+
//
|
|
59
|
+
|
|
60
|
+
var instance *CLITelemetry
|
|
61
|
+
var once sync.Once
|
|
62
|
+
|
|
63
|
+
//
|
|
64
|
+
// Private functions
|
|
65
|
+
//
|
|
66
|
+
|
|
67
|
+
func getTelemetryHeader() (string, error) {
|
|
68
|
+
telemetry := GetTelemetryInstance()
|
|
69
|
+
b, err := json.Marshal(telemetry)
|
|
70
|
+
|
|
71
|
+
if err != nil {
|
|
72
|
+
return "", err
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return string(b), nil
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// telemetryOptedOut returns true if the user has opted out of telemetry,
|
|
79
|
+
// false otherwise.
|
|
80
|
+
func telemetryOptedOut(optoutVar string) bool {
|
|
81
|
+
optoutVar = strings.ToLower(optoutVar)
|
|
82
|
+
|
|
83
|
+
return optoutVar == "1" || optoutVar == "true"
|
|
84
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package hookdeck
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
|
|
6
|
+
"github.com/spf13/cobra"
|
|
7
|
+
"github.com/stretchr/testify/require"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestGetTelemetryInstance(t *testing.T) {
|
|
11
|
+
t1 := GetTelemetryInstance()
|
|
12
|
+
t2 := GetTelemetryInstance()
|
|
13
|
+
require.Equal(t, t1, t2)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func TestSetCommandContext(t *testing.T) {
|
|
17
|
+
tel := GetTelemetryInstance()
|
|
18
|
+
cmd := &cobra.Command{
|
|
19
|
+
Use: "foo",
|
|
20
|
+
}
|
|
21
|
+
tel.SetCommandContext(cmd)
|
|
22
|
+
require.Equal(t, "foo", tel.CommandPath)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func TestTelemetryOptedOut(t *testing.T) {
|
|
26
|
+
require.False(t, telemetryOptedOut(""))
|
|
27
|
+
require.False(t, telemetryOptedOut("0"))
|
|
28
|
+
require.False(t, telemetryOptedOut("false"))
|
|
29
|
+
require.False(t, telemetryOptedOut("False"))
|
|
30
|
+
require.False(t, telemetryOptedOut("FALSE"))
|
|
31
|
+
require.True(t, telemetryOptedOut("1"))
|
|
32
|
+
require.True(t, telemetryOptedOut("true"))
|
|
33
|
+
require.True(t, telemetryOptedOut("True"))
|
|
34
|
+
require.True(t, telemetryOptedOut("TRUE"))
|
|
35
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package hookdeck
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"io"
|
|
6
|
+
"net/http"
|
|
7
|
+
"regexp"
|
|
8
|
+
"strings"
|
|
9
|
+
|
|
10
|
+
"github.com/hookdeck/hookdeck-cli/pkg/ansi"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// inspectHeaders is the whitelist of headers that will be printed.
|
|
14
|
+
var inspectHeaders = []string{
|
|
15
|
+
"Authorization",
|
|
16
|
+
"Content-Type",
|
|
17
|
+
"Date",
|
|
18
|
+
"Idempotency-Key",
|
|
19
|
+
"Idempotency-Replayed",
|
|
20
|
+
"Request-Id",
|
|
21
|
+
"Hookdeck-Account",
|
|
22
|
+
"Hookdeck-Version",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type verboseTransport struct {
|
|
26
|
+
Transport *http.Transport
|
|
27
|
+
Verbose bool
|
|
28
|
+
Out io.Writer
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func (t *verboseTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
|
32
|
+
if t.Verbose {
|
|
33
|
+
t.dumpRequest(req)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
resp, err = t.Transport.RoundTrip(req)
|
|
37
|
+
|
|
38
|
+
if err == nil && t.Verbose {
|
|
39
|
+
t.dumpResponse(resp)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func (t *verboseTransport) dumpRequest(req *http.Request) {
|
|
46
|
+
info := fmt.Sprintf("> %s %s://%s%s", req.Method, req.URL.Scheme, req.URL.Host, req.URL.RequestURI())
|
|
47
|
+
t.verbosePrintln(info)
|
|
48
|
+
t.dumpHeaders(req.Header, ">")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func (t *verboseTransport) dumpResponse(resp *http.Response) {
|
|
52
|
+
info := fmt.Sprintf("< HTTP %d", resp.StatusCode)
|
|
53
|
+
t.verbosePrintln(info)
|
|
54
|
+
t.dumpHeaders(resp.Header, "<")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func (t *verboseTransport) dumpHeaders(header http.Header, indent string) {
|
|
58
|
+
for _, listed := range inspectHeaders {
|
|
59
|
+
for name, vv := range header {
|
|
60
|
+
if !strings.EqualFold(name, listed) {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for _, v := range vv {
|
|
65
|
+
if v != "" {
|
|
66
|
+
r := regexp.MustCompile("(?i)^(basic|bearer) (.+)")
|
|
67
|
+
if r.MatchString(v) {
|
|
68
|
+
v = r.ReplaceAllString(v, "$1 [REDACTED]")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
info := fmt.Sprintf("%s %s: %s", indent, name, v)
|
|
72
|
+
t.verbosePrintln(info)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func (t *verboseTransport) verbosePrintln(msg string) {
|
|
80
|
+
color := ansi.Color(t.Out)
|
|
81
|
+
fmt.Fprintln(t.Out, color.Cyan(msg))
|
|
82
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package hookdeck
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"net/http"
|
|
6
|
+
"net/http/httptest"
|
|
7
|
+
"regexp"
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
"github.com/stretchr/testify/require"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func TestVerboseTransport_Verbose(t *testing.T) {
|
|
14
|
+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
15
|
+
w.Header().Set("Request-Id", "req_123")
|
|
16
|
+
w.Header().Set("Non-Whitelisted-Header", "foo")
|
|
17
|
+
w.WriteHeader(http.StatusOK)
|
|
18
|
+
}))
|
|
19
|
+
defer ts.Close()
|
|
20
|
+
|
|
21
|
+
var b bytes.Buffer
|
|
22
|
+
|
|
23
|
+
httpTransport := &http.Transport{}
|
|
24
|
+
tr := &verboseTransport{
|
|
25
|
+
Transport: httpTransport,
|
|
26
|
+
Verbose: true,
|
|
27
|
+
Out: &b,
|
|
28
|
+
}
|
|
29
|
+
client := &http.Client{Transport: tr}
|
|
30
|
+
req, err := http.NewRequest("POST", ts.URL+"/test", nil)
|
|
31
|
+
require.NoError(t, err)
|
|
32
|
+
req.Header.Set("Authorization", "Bearer token")
|
|
33
|
+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
34
|
+
|
|
35
|
+
resp, err := client.Do(req)
|
|
36
|
+
require.NoError(t, err)
|
|
37
|
+
|
|
38
|
+
defer resp.Body.Close()
|
|
39
|
+
|
|
40
|
+
out := b.String()
|
|
41
|
+
require.Regexp(t, regexp.MustCompile("> POST http://(.+)/test\n"), out)
|
|
42
|
+
require.Contains(t, out, "> Authorization: Bearer [REDACTED]\n")
|
|
43
|
+
require.Contains(t, out, "> Content-Type: application/x-www-form-urlencoded\n")
|
|
44
|
+
require.Contains(t, out, "< HTTP 200\n")
|
|
45
|
+
require.Contains(t, out, "< Request-Id: req_123\n")
|
|
46
|
+
require.NotContains(t, out, "Non-Whitelisted-Header")
|
|
47
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
package listen
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"errors"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/AlecAivazis/survey/v2"
|
|
9
|
+
"github.com/gosimple/slug"
|
|
10
|
+
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func getConnections(client *hookdeck.Client, source hookdeck.Source, connection_query string) ([]hookdeck.Connection, error) {
|
|
14
|
+
// TODO: Filter connections using connection_query
|
|
15
|
+
var connections []hookdeck.Connection
|
|
16
|
+
connections, err := client.ListConnectionsBySource(source.Id)
|
|
17
|
+
if err != nil {
|
|
18
|
+
return connections, err
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var filtered_connections []hookdeck.Connection
|
|
22
|
+
for _, connection := range connections {
|
|
23
|
+
if connection.Destination.CliPath != "" {
|
|
24
|
+
filtered_connections = append(filtered_connections, connection)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
connections = filtered_connections
|
|
28
|
+
|
|
29
|
+
if connection_query != "" {
|
|
30
|
+
is_path, err := isPath(connection_query)
|
|
31
|
+
if err != nil {
|
|
32
|
+
return connections, err
|
|
33
|
+
}
|
|
34
|
+
var filtered_connections []hookdeck.Connection
|
|
35
|
+
for _, connection := range connections {
|
|
36
|
+
if (is_path && strings.Contains(connection.Destination.CliPath, connection_query)) || connection.Alias == connection_query {
|
|
37
|
+
filtered_connections = append(filtered_connections, connection)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
connections = filtered_connections
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if len(connections) == 0 {
|
|
44
|
+
answers := struct {
|
|
45
|
+
Label string `survey:"label"`
|
|
46
|
+
Path string `survey:"path"`
|
|
47
|
+
}{}
|
|
48
|
+
var qs = []*survey.Question{
|
|
49
|
+
{
|
|
50
|
+
Name: "path",
|
|
51
|
+
Prompt: &survey.Input{Message: "What path should the webhooks be forwarded to (ie: /webhooks)?"},
|
|
52
|
+
Validate: func(val interface{}) error {
|
|
53
|
+
str, ok := val.(string)
|
|
54
|
+
is_path, err := isPath(str)
|
|
55
|
+
if !ok || !is_path || err != nil {
|
|
56
|
+
return errors.New("invalid path")
|
|
57
|
+
}
|
|
58
|
+
return nil
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
Name: "label",
|
|
63
|
+
Prompt: &survey.Input{Message: "What's your connection label (ie: My API)?"},
|
|
64
|
+
Validate: survey.Required,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
err := survey.Ask(qs, &answers)
|
|
69
|
+
if err != nil {
|
|
70
|
+
fmt.Println(err.Error())
|
|
71
|
+
return connections, err
|
|
72
|
+
}
|
|
73
|
+
alias := slug.Make(answers.Label)
|
|
74
|
+
connection, err := client.CreateConnection(hookdeck.CreateConnectionInput{
|
|
75
|
+
Alias: alias,
|
|
76
|
+
Label: answers.Label,
|
|
77
|
+
SourceId: source.Id,
|
|
78
|
+
Destination: hookdeck.CreateDestinationInput{
|
|
79
|
+
Alias: alias,
|
|
80
|
+
Label: answers.Label,
|
|
81
|
+
CliPath: answers.Path,
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
if err != nil {
|
|
85
|
+
return connections, err
|
|
86
|
+
}
|
|
87
|
+
connections = append(connections, connection)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return connections, nil
|
|
91
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright © 2020 NAME HERE <EMAIL ADDRESS>
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
package listen
|
|
17
|
+
|
|
18
|
+
import (
|
|
19
|
+
"context"
|
|
20
|
+
"fmt"
|
|
21
|
+
"net/url"
|
|
22
|
+
"regexp"
|
|
23
|
+
|
|
24
|
+
"github.com/hookdeck/hookdeck-cli/pkg/ansi"
|
|
25
|
+
"github.com/hookdeck/hookdeck-cli/pkg/config"
|
|
26
|
+
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
|
|
27
|
+
"github.com/hookdeck/hookdeck-cli/pkg/login"
|
|
28
|
+
"github.com/hookdeck/hookdeck-cli/pkg/proxy"
|
|
29
|
+
"github.com/hookdeck/hookdeck-cli/pkg/validators"
|
|
30
|
+
log "github.com/sirupsen/logrus"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
type Flags struct {
|
|
34
|
+
NoWSS bool
|
|
35
|
+
WSBaseURL string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// listenCmd represents the listen command
|
|
39
|
+
func Listen(URL *url.URL, source_alias string, connection_query string, flags Flags, config *config.Config) error {
|
|
40
|
+
var key string
|
|
41
|
+
var err error
|
|
42
|
+
var guest_url string
|
|
43
|
+
|
|
44
|
+
key, err = config.Profile.GetAPIKey()
|
|
45
|
+
if err != nil {
|
|
46
|
+
errString := err.Error()
|
|
47
|
+
if errString == validators.ErrAPIKeyNotConfigured.Error() || errString == validators.ErrDeviceNameNotConfigured.Error() {
|
|
48
|
+
guest_url, _ = login.GuestLogin(config)
|
|
49
|
+
if guest_url == "" {
|
|
50
|
+
return err
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
key, err = config.Profile.GetAPIKey()
|
|
54
|
+
if err != nil {
|
|
55
|
+
return err
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
return err
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
parsedBaseURL, err := url.Parse(config.APIBaseURL)
|
|
63
|
+
if err != nil {
|
|
64
|
+
return err
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
client := &hookdeck.Client{
|
|
68
|
+
BaseURL: parsedBaseURL,
|
|
69
|
+
APIKey: key,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
source, err := getSource(client, source_alias)
|
|
73
|
+
if err != nil {
|
|
74
|
+
return err
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
connections, err := getConnections(client, source, connection_query)
|
|
78
|
+
if err != nil {
|
|
79
|
+
return err
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fmt.Println()
|
|
83
|
+
fmt.Println(ansi.Bold("Dashboard"))
|
|
84
|
+
if guest_url != "" {
|
|
85
|
+
fmt.Println("👤 Console URL: " + guest_url)
|
|
86
|
+
fmt.Println("Sign up in the Console to make your webhook URL permanent.")
|
|
87
|
+
fmt.Println()
|
|
88
|
+
} else {
|
|
89
|
+
var url = config.DashboardBaseURL
|
|
90
|
+
if config.Profile.GetTeamId() != "" {
|
|
91
|
+
url += "?team_id=" + config.Profile.GetTeamId()
|
|
92
|
+
}
|
|
93
|
+
if config.Profile.GetTeamMode() == "console" {
|
|
94
|
+
url = config.ConsoleBaseURL + "?source_id=" + source.Id
|
|
95
|
+
}
|
|
96
|
+
fmt.Println("👉 Inspect and replay webhooks: " + url)
|
|
97
|
+
fmt.Println()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fmt.Println(ansi.Bold(source.Label + " Source"))
|
|
101
|
+
fmt.Println("🔌 Webhook URL: " + source.Url)
|
|
102
|
+
fmt.Println()
|
|
103
|
+
|
|
104
|
+
fmt.Println(ansi.Bold("Connections"))
|
|
105
|
+
for _, connection := range connections {
|
|
106
|
+
fmt.Println(connection.Label + " forwarding to " + connection.Destination.CliPath)
|
|
107
|
+
}
|
|
108
|
+
fmt.Println()
|
|
109
|
+
|
|
110
|
+
deviceName, err := config.Profile.GetDeviceName()
|
|
111
|
+
if err != nil {
|
|
112
|
+
return err
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
p := proxy.New(&proxy.Config{
|
|
116
|
+
DeviceName: deviceName,
|
|
117
|
+
Key: key,
|
|
118
|
+
APIBaseURL: config.APIBaseURL,
|
|
119
|
+
DashboardBaseURL: config.DashboardBaseURL,
|
|
120
|
+
ConsoleBaseURL: config.ConsoleBaseURL,
|
|
121
|
+
Profile: config.Profile,
|
|
122
|
+
WSBaseURL: flags.WSBaseURL,
|
|
123
|
+
NoWSS: flags.NoWSS,
|
|
124
|
+
URL: URL,
|
|
125
|
+
Log: log.StandardLogger(),
|
|
126
|
+
Insecure: config.Insecure,
|
|
127
|
+
}, source, connections)
|
|
128
|
+
|
|
129
|
+
err = p.Run(context.Background())
|
|
130
|
+
if err != nil {
|
|
131
|
+
return err
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return nil
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func isPath(value string) (bool, error) {
|
|
138
|
+
is_path, err := regexp.MatchString("^(/)+([/.a-zA-Z0-9-_]*)$", value)
|
|
139
|
+
return is_path, err
|
|
140
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package listen
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
|
|
6
|
+
"github.com/AlecAivazis/survey/v2"
|
|
7
|
+
"github.com/gosimple/slug"
|
|
8
|
+
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func getSource(client *hookdeck.Client, source_alias string) (hookdeck.Source, error) {
|
|
12
|
+
var source hookdeck.Source
|
|
13
|
+
if source_alias != "" {
|
|
14
|
+
source, _ = client.GetSourceByAlias(source_alias)
|
|
15
|
+
if source.Id == "" {
|
|
16
|
+
// TODO: Prompt here?
|
|
17
|
+
source, _ = client.CreateSource(hookdeck.CreateSourceInput{
|
|
18
|
+
Alias: source_alias,
|
|
19
|
+
// TODO: labelized alias
|
|
20
|
+
Label: source_alias,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
sources, _ := client.ListSources()
|
|
25
|
+
if len(sources) > 0 {
|
|
26
|
+
var sources_alias []string
|
|
27
|
+
for _, temp_source := range sources {
|
|
28
|
+
sources_alias = append(sources_alias, temp_source.Alias)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
answers := struct {
|
|
32
|
+
SourceAlias string `survey:"source"`
|
|
33
|
+
}{}
|
|
34
|
+
|
|
35
|
+
var qs = []*survey.Question{
|
|
36
|
+
{
|
|
37
|
+
Name: "source",
|
|
38
|
+
Prompt: &survey.Select{
|
|
39
|
+
Message: "Select a source",
|
|
40
|
+
Options: append(sources_alias, "Create new source"),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
err := survey.Ask(qs, &answers)
|
|
46
|
+
if err != nil {
|
|
47
|
+
fmt.Println(err.Error())
|
|
48
|
+
return source, err
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if answers.SourceAlias != "Create new source" {
|
|
52
|
+
for _, temp_source := range sources {
|
|
53
|
+
if temp_source.Alias == answers.SourceAlias {
|
|
54
|
+
source = temp_source
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if source.Id == "" {
|
|
61
|
+
answers := struct {
|
|
62
|
+
Label string `survey:"label"` // or you can tag fields to match a specific name
|
|
63
|
+
}{}
|
|
64
|
+
var qs = []*survey.Question{
|
|
65
|
+
{
|
|
66
|
+
Name: "label",
|
|
67
|
+
Prompt: &survey.Input{Message: "What should be your new source label?"},
|
|
68
|
+
Validate: survey.Required,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
err := survey.Ask(qs, &answers)
|
|
73
|
+
if err != nil {
|
|
74
|
+
return source, err
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
source, _ = client.CreateSource(hookdeck.CreateSourceInput{
|
|
78
|
+
Alias: slug.Make(answers.Label),
|
|
79
|
+
Label: answers.Label,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return source, nil
|
|
84
|
+
}
|