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,525 @@
|
|
|
1
|
+
package websocket
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/base64"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"errors"
|
|
8
|
+
"io/ioutil"
|
|
9
|
+
"net"
|
|
10
|
+
"net/http"
|
|
11
|
+
"os"
|
|
12
|
+
"strings"
|
|
13
|
+
"sync"
|
|
14
|
+
"time"
|
|
15
|
+
|
|
16
|
+
ws "github.com/gorilla/websocket"
|
|
17
|
+
log "github.com/sirupsen/logrus"
|
|
18
|
+
|
|
19
|
+
"github.com/hookdeck/hookdeck-cli/pkg/useragent"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
//
|
|
23
|
+
// Public types
|
|
24
|
+
//
|
|
25
|
+
|
|
26
|
+
// Config contains the optional configuration parameters of a Client.
|
|
27
|
+
type Config struct {
|
|
28
|
+
ConnectAttemptWait time.Duration
|
|
29
|
+
|
|
30
|
+
Dialer *ws.Dialer
|
|
31
|
+
|
|
32
|
+
Log *log.Logger
|
|
33
|
+
|
|
34
|
+
// Force use of unencrypted ws:// protocol instead of wss://
|
|
35
|
+
NoWSS bool
|
|
36
|
+
|
|
37
|
+
PingPeriod time.Duration
|
|
38
|
+
|
|
39
|
+
PongWait time.Duration
|
|
40
|
+
|
|
41
|
+
WriteWait time.Duration
|
|
42
|
+
|
|
43
|
+
EventHandler EventHandler
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// EventHandler handles an event.
|
|
47
|
+
type EventHandler interface {
|
|
48
|
+
ProcessEvent(IncomingMessage)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// EventHandlerFunc is an adapter to allow the use of ordinary
|
|
52
|
+
// functions as event handlers. If f is a function with the
|
|
53
|
+
// appropriate signature, EventHandlerFunc(f) is a
|
|
54
|
+
// EventHandler that calls f.
|
|
55
|
+
type EventHandlerFunc func(IncomingMessage)
|
|
56
|
+
|
|
57
|
+
// ProcessEvent calls f(msg).
|
|
58
|
+
func (f EventHandlerFunc) ProcessEvent(msg IncomingMessage) {
|
|
59
|
+
f(msg)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Client is the client used to receive webhook requests from Hookdeck
|
|
63
|
+
// and send back webhook responses from the local endpoint to Hookdeck.
|
|
64
|
+
type Client struct {
|
|
65
|
+
// URL the client connects to
|
|
66
|
+
URL string
|
|
67
|
+
|
|
68
|
+
CLIKey string
|
|
69
|
+
|
|
70
|
+
// ID sent by the client in the `Websocket-Id` header when connecting
|
|
71
|
+
WebSocketID string
|
|
72
|
+
|
|
73
|
+
// Feature that the websocket is specified for
|
|
74
|
+
//WebSocketAuthorizedFeature string
|
|
75
|
+
|
|
76
|
+
// Optional configuration parameters
|
|
77
|
+
cfg *Config
|
|
78
|
+
|
|
79
|
+
conn *ws.Conn
|
|
80
|
+
done chan struct{}
|
|
81
|
+
isConnected bool
|
|
82
|
+
|
|
83
|
+
NotifyExpired chan struct{}
|
|
84
|
+
notifyClose chan error
|
|
85
|
+
send chan *OutgoingMessage
|
|
86
|
+
stopReadPump chan struct{}
|
|
87
|
+
stopWritePump chan struct{}
|
|
88
|
+
wg *sync.WaitGroup
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Connected returns a channel that's closed when the client has finished
|
|
92
|
+
// establishing the websocket connection.
|
|
93
|
+
func (c *Client) Connected() <-chan struct{} {
|
|
94
|
+
d := make(chan struct{})
|
|
95
|
+
|
|
96
|
+
go func() {
|
|
97
|
+
for !c.isConnected {
|
|
98
|
+
time.Sleep(100 * time.Millisecond)
|
|
99
|
+
}
|
|
100
|
+
close(d)
|
|
101
|
+
}()
|
|
102
|
+
|
|
103
|
+
return d
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Run starts listening for incoming webhook requests from Hookdeck.
|
|
107
|
+
func (c *Client) Run(ctx context.Context) {
|
|
108
|
+
c.isConnected = false
|
|
109
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
110
|
+
"prefix": "websocket.client.Run",
|
|
111
|
+
}).Debug("Attempting to connect to Hookdeck")
|
|
112
|
+
|
|
113
|
+
err := c.connect(ctx)
|
|
114
|
+
if err != nil {
|
|
115
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
116
|
+
"prefix": "websocket.client.Run",
|
|
117
|
+
}).Debug(err)
|
|
118
|
+
|
|
119
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
120
|
+
"prefix": "websocket.client.Run",
|
|
121
|
+
}).Debug("Failed to connect to Hookdeck. Retrying...")
|
|
122
|
+
|
|
123
|
+
if err == ErrUnknownID {
|
|
124
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
125
|
+
"prefix": "websocket.client.Run",
|
|
126
|
+
}).Debug("Websocket session is expired.")
|
|
127
|
+
}
|
|
128
|
+
c.ConnectionLost()
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
select {
|
|
133
|
+
case <-ctx.Done():
|
|
134
|
+
close(c.send)
|
|
135
|
+
close(c.stopReadPump)
|
|
136
|
+
close(c.stopWritePump)
|
|
137
|
+
|
|
138
|
+
return
|
|
139
|
+
case <-c.done:
|
|
140
|
+
close(c.send)
|
|
141
|
+
close(c.stopReadPump)
|
|
142
|
+
close(c.stopWritePump)
|
|
143
|
+
close(c.NotifyExpired)
|
|
144
|
+
|
|
145
|
+
return
|
|
146
|
+
case <-c.notifyClose:
|
|
147
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
148
|
+
"prefix": "websocket.client.Run",
|
|
149
|
+
}).Debug("Disconnected from Hookdeck")
|
|
150
|
+
|
|
151
|
+
c.ConnectionLost()
|
|
152
|
+
close(c.stopReadPump)
|
|
153
|
+
close(c.stopWritePump)
|
|
154
|
+
c.wg.Wait()
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ConnectionLost sends NotifyExpired
|
|
159
|
+
func (c *Client) ConnectionLost() {
|
|
160
|
+
c.NotifyExpired <- struct{}{}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Stop stops listening for incoming webhook events.
|
|
164
|
+
func (c *Client) Stop() {
|
|
165
|
+
close(c.done)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// SendMessage sends a message to Hookdeck through the websocket.
|
|
169
|
+
func (c *Client) SendMessage(msg *OutgoingMessage) {
|
|
170
|
+
c.send <- msg
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func readWSConnectErrorMessage(resp *http.Response) string {
|
|
174
|
+
if resp == nil {
|
|
175
|
+
return ""
|
|
176
|
+
}
|
|
177
|
+
if resp.Body == nil {
|
|
178
|
+
return ""
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
se := struct {
|
|
182
|
+
InnerError struct {
|
|
183
|
+
Message string `json:"message"`
|
|
184
|
+
} `json:"error"`
|
|
185
|
+
}{}
|
|
186
|
+
|
|
187
|
+
body, err := ioutil.ReadAll(resp.Body)
|
|
188
|
+
|
|
189
|
+
if err != nil {
|
|
190
|
+
return ""
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
err = json.Unmarshal(body, &se)
|
|
194
|
+
if err != nil {
|
|
195
|
+
return ""
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return se.InnerError.Message
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var unknownIDMessage string = "Unknown WebSocket ID."
|
|
202
|
+
|
|
203
|
+
// ErrUnknownID can occur when the websocket session is expired or invalid
|
|
204
|
+
var ErrUnknownID error = errors.New(unknownIDMessage)
|
|
205
|
+
|
|
206
|
+
func basicAuth(username, password string) string {
|
|
207
|
+
auth := username + ":" + password
|
|
208
|
+
return base64.StdEncoding.EncodeToString([]byte(auth))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// connect makes a single attempt to connect to the websocket URL. It returns
|
|
212
|
+
// the success of the attempt.
|
|
213
|
+
func (c *Client) connect(ctx context.Context) error {
|
|
214
|
+
header := http.Header{}
|
|
215
|
+
// Disable compression by requiring "identity"
|
|
216
|
+
header.Set("Accept-Encoding", "identity")
|
|
217
|
+
header.Set("User-Agent", useragent.GetEncodedUserAgent())
|
|
218
|
+
header.Set("X-Hookdeck-Client-User-Agent", useragent.GetEncodedHookdeckUserAgent())
|
|
219
|
+
header.Set("Websocket-Id", c.WebSocketID)
|
|
220
|
+
header.Set("Authorization", "Basic "+basicAuth(c.CLIKey, ""))
|
|
221
|
+
|
|
222
|
+
url := c.URL
|
|
223
|
+
if c.cfg.NoWSS && strings.HasPrefix(url, "wss") {
|
|
224
|
+
url = "ws" + strings.TrimPrefix(c.URL, "wss")
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
228
|
+
"prefix": "websocket.Client.connect",
|
|
229
|
+
"url": url,
|
|
230
|
+
}).Debug("Dialing websocket")
|
|
231
|
+
|
|
232
|
+
conn, resp, err := c.cfg.Dialer.DialContext(ctx, url, header)
|
|
233
|
+
if err != nil {
|
|
234
|
+
message := readWSConnectErrorMessage(resp)
|
|
235
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
236
|
+
"prefix": "websocket.Client.connect",
|
|
237
|
+
"error": err,
|
|
238
|
+
"message": message,
|
|
239
|
+
}).Debug("Websocket connection error")
|
|
240
|
+
if message == unknownIDMessage {
|
|
241
|
+
return ErrUnknownID
|
|
242
|
+
}
|
|
243
|
+
return err
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
defer resp.Body.Close()
|
|
247
|
+
|
|
248
|
+
c.changeConnection(conn)
|
|
249
|
+
c.isConnected = true
|
|
250
|
+
|
|
251
|
+
c.wg = &sync.WaitGroup{}
|
|
252
|
+
c.wg.Add(2)
|
|
253
|
+
|
|
254
|
+
go c.readPump()
|
|
255
|
+
|
|
256
|
+
go c.writePump()
|
|
257
|
+
|
|
258
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
259
|
+
"prefix": "websocket.client.connect",
|
|
260
|
+
}).Debug("Connected!")
|
|
261
|
+
|
|
262
|
+
return err
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// changeConnection takes a new connection and recreates the channels.
|
|
266
|
+
func (c *Client) changeConnection(conn *ws.Conn) {
|
|
267
|
+
c.conn = conn
|
|
268
|
+
c.notifyClose = make(chan error)
|
|
269
|
+
c.stopReadPump = make(chan struct{})
|
|
270
|
+
c.stopWritePump = make(chan struct{})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// readPump pumps messages from the websocket connection and pushes them into
|
|
274
|
+
// RequestHandler's ProcessWebhookRequest.
|
|
275
|
+
//
|
|
276
|
+
// The application runs readPump in a per-connection goroutine. The application
|
|
277
|
+
// ensures that there is at most one reader on a connection by executing all
|
|
278
|
+
// reads from this goroutine.
|
|
279
|
+
func (c *Client) readPump() {
|
|
280
|
+
defer c.wg.Done()
|
|
281
|
+
|
|
282
|
+
err := c.conn.SetReadDeadline(time.Now().Add(c.cfg.PongWait))
|
|
283
|
+
if err != nil {
|
|
284
|
+
c.cfg.Log.Debug("SetReadDeadline error: ", err)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
c.conn.SetPongHandler(func(string) error {
|
|
288
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
289
|
+
"prefix": "websocket.Client.readPump",
|
|
290
|
+
}).Debug("Received pong message")
|
|
291
|
+
|
|
292
|
+
err := c.conn.SetReadDeadline(time.Now().Add(c.cfg.PongWait))
|
|
293
|
+
if err != nil {
|
|
294
|
+
c.cfg.Log.Debug("SetReadDeadline error: ", err)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return nil
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
for {
|
|
301
|
+
_, data, err := c.conn.ReadMessage()
|
|
302
|
+
if err != nil {
|
|
303
|
+
select {
|
|
304
|
+
case <-c.stopReadPump:
|
|
305
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
306
|
+
"prefix": "websocket.Client.readPump",
|
|
307
|
+
}).Debug("stopReadPump")
|
|
308
|
+
default:
|
|
309
|
+
switch {
|
|
310
|
+
case !ws.IsCloseError(err):
|
|
311
|
+
// read errors do not prevent websocket reconnects in the CLI so we should
|
|
312
|
+
// only display this on debug-level logging
|
|
313
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
314
|
+
"prefix": "websocket.Client.Close",
|
|
315
|
+
}).Debug("read error: ", err)
|
|
316
|
+
case ws.IsUnexpectedCloseError(err, ws.CloseNormalClosure):
|
|
317
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
318
|
+
"prefix": "websocket.Client.Close",
|
|
319
|
+
}).Error("close error: ", err)
|
|
320
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
321
|
+
"prefix": "hookdeckcli.ADDITIONAL_INFO",
|
|
322
|
+
}).Error("If you run into issues, please re-run with `--log-level debug` and share the output with the Hookdeck team on GitHub.")
|
|
323
|
+
default:
|
|
324
|
+
c.cfg.Log.Error("other error: ", err)
|
|
325
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
326
|
+
"prefix": "hookdeckcli.ADDITIONAL_INFO",
|
|
327
|
+
}).Error("If you run into issues, please re-run with `--log-level debug` and share the output with the Hookdeck team on GitHub.")
|
|
328
|
+
}
|
|
329
|
+
c.notifyClose <- err
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
336
|
+
"prefix": "websocket.Client.readPump",
|
|
337
|
+
"message": string(data),
|
|
338
|
+
}).Debug("Incoming message")
|
|
339
|
+
|
|
340
|
+
var msg IncomingMessage
|
|
341
|
+
if err = json.Unmarshal(data, &msg); err != nil {
|
|
342
|
+
c.cfg.Log.Debug("Received malformed message: ", err)
|
|
343
|
+
|
|
344
|
+
continue
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
go c.cfg.EventHandler.ProcessEvent(msg)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// writePump pumps messages to the websocket connection that are queued with
|
|
352
|
+
// SendWebhookResponse.
|
|
353
|
+
//
|
|
354
|
+
// A goroutine running writePump is started for each connection. The
|
|
355
|
+
// application ensures that there is at most one writer to a connection by
|
|
356
|
+
// executing all writes from this goroutine.
|
|
357
|
+
func (c *Client) writePump() {
|
|
358
|
+
ticker := time.NewTicker(c.cfg.PingPeriod)
|
|
359
|
+
defer func() {
|
|
360
|
+
ticker.Stop()
|
|
361
|
+
c.wg.Done()
|
|
362
|
+
}()
|
|
363
|
+
|
|
364
|
+
for {
|
|
365
|
+
select {
|
|
366
|
+
case whResp, ok := <-c.send:
|
|
367
|
+
err := c.conn.SetWriteDeadline(time.Now().Add(c.cfg.WriteWait))
|
|
368
|
+
if err != nil {
|
|
369
|
+
c.cfg.Log.Debug("SetWriteDeadline error: ", err)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if !ok {
|
|
373
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
374
|
+
"prefix": "websocket.Client.writePump",
|
|
375
|
+
}).Debug("Sending close message")
|
|
376
|
+
|
|
377
|
+
err = c.conn.WriteMessage(ws.CloseMessage, ws.FormatCloseMessage(ws.CloseNormalClosure, ""))
|
|
378
|
+
if err != nil {
|
|
379
|
+
c.cfg.Log.Debug("WriteMessage error: ", err)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
386
|
+
"prefix": "websocket.Client.writePump",
|
|
387
|
+
}).Debug("Sending text message")
|
|
388
|
+
|
|
389
|
+
err = c.conn.WriteJSON(whResp)
|
|
390
|
+
if err != nil {
|
|
391
|
+
if ws.IsUnexpectedCloseError(err, ws.CloseNormalClosure) {
|
|
392
|
+
c.cfg.Log.Error("write error: ", err)
|
|
393
|
+
}
|
|
394
|
+
// Requeue the message to be processed when writePump restarts
|
|
395
|
+
c.send <- whResp
|
|
396
|
+
c.notifyClose <- err
|
|
397
|
+
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
case <-ticker.C:
|
|
401
|
+
err := c.conn.SetWriteDeadline(time.Now().Add(c.cfg.WriteWait))
|
|
402
|
+
if err != nil {
|
|
403
|
+
c.cfg.Log.Debug("SetWriteDeadline error: ", err)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
407
|
+
"prefix": "websocket.Client.writePump",
|
|
408
|
+
}).Debug("Sending ping message")
|
|
409
|
+
|
|
410
|
+
if err = c.conn.WriteMessage(ws.PingMessage, nil); err != nil {
|
|
411
|
+
if ws.IsUnexpectedCloseError(err, ws.CloseNormalClosure) {
|
|
412
|
+
c.cfg.Log.Error("write error: ", err)
|
|
413
|
+
}
|
|
414
|
+
c.notifyClose <- err
|
|
415
|
+
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
case <-c.stopWritePump:
|
|
419
|
+
c.cfg.Log.WithFields(log.Fields{
|
|
420
|
+
"prefix": "websocket.Client.writePump",
|
|
421
|
+
}).Debug("stopWritePump")
|
|
422
|
+
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
//
|
|
429
|
+
// Public functions
|
|
430
|
+
//
|
|
431
|
+
|
|
432
|
+
// NewClient returns a new Client.
|
|
433
|
+
func NewClient(url string, webSocketID string, CLIKey string, cfg *Config) *Client {
|
|
434
|
+
if cfg == nil {
|
|
435
|
+
cfg = &Config{}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if cfg.ConnectAttemptWait == 0 {
|
|
439
|
+
cfg.ConnectAttemptWait = defaultConnectAttemptWait
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if cfg.Dialer == nil {
|
|
443
|
+
cfg.Dialer = newWebSocketDialer(os.Getenv("HOOKDECK_CLI_UNIX_SOCKET"))
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if cfg.Log == nil {
|
|
447
|
+
cfg.Log = &log.Logger{Out: ioutil.Discard}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if cfg.PongWait == 0 {
|
|
451
|
+
cfg.PongWait = defaultPongWait
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if cfg.PingPeriod == 0 {
|
|
455
|
+
cfg.PingPeriod = (cfg.PongWait * 9) / 10
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if cfg.WriteWait == 0 {
|
|
459
|
+
cfg.WriteWait = defaultWriteWait
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if cfg.EventHandler == nil {
|
|
463
|
+
cfg.EventHandler = nullEventHandler
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Note that this client is not configured for websocket communications
|
|
467
|
+
// and you must call c.changeConnection
|
|
468
|
+
return &Client{
|
|
469
|
+
URL: url,
|
|
470
|
+
WebSocketID: webSocketID,
|
|
471
|
+
// WebSocketAuthorizedFeature: websocketAuthorizedFeature,
|
|
472
|
+
CLIKey: CLIKey,
|
|
473
|
+
cfg: cfg,
|
|
474
|
+
done: make(chan struct{}),
|
|
475
|
+
send: make(chan *OutgoingMessage),
|
|
476
|
+
NotifyExpired: make(chan struct{}),
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
//
|
|
481
|
+
// Private constants
|
|
482
|
+
//
|
|
483
|
+
|
|
484
|
+
const (
|
|
485
|
+
defaultConnectAttemptWait = 10 * time.Second
|
|
486
|
+
|
|
487
|
+
defaultPongWait = 10 * time.Second
|
|
488
|
+
|
|
489
|
+
defaultWriteWait = 10 * time.Second
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
//
|
|
493
|
+
// Private variables
|
|
494
|
+
//
|
|
495
|
+
|
|
496
|
+
var subprotocols = [...]string{"hookdeckcli-devproxy-v1"}
|
|
497
|
+
|
|
498
|
+
var nullEventHandler = EventHandlerFunc(func(IncomingMessage) {})
|
|
499
|
+
|
|
500
|
+
//
|
|
501
|
+
// Private functions
|
|
502
|
+
//
|
|
503
|
+
|
|
504
|
+
func newWebSocketDialer(unixSocket string) *ws.Dialer {
|
|
505
|
+
var dialer *ws.Dialer
|
|
506
|
+
|
|
507
|
+
if unixSocket != "" {
|
|
508
|
+
dialFunc := func(network, addr string) (net.Conn, error) {
|
|
509
|
+
return net.Dial("unix", unixSocket)
|
|
510
|
+
}
|
|
511
|
+
dialer = &ws.Dialer{
|
|
512
|
+
HandshakeTimeout: 10 * time.Second,
|
|
513
|
+
NetDial: dialFunc,
|
|
514
|
+
Subprotocols: subprotocols[:],
|
|
515
|
+
}
|
|
516
|
+
} else {
|
|
517
|
+
dialer = &ws.Dialer{
|
|
518
|
+
HandshakeTimeout: 10 * time.Second,
|
|
519
|
+
Proxy: http.ProxyFromEnvironment,
|
|
520
|
+
Subprotocols: subprotocols[:],
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return dialer
|
|
525
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package websocket
|
|
2
|
+
|
|
3
|
+
type ConnectionMessageBody struct {
|
|
4
|
+
SourceId string `json:"source_id"`
|
|
5
|
+
ConnectionIds []string `json:"webhook_ids"`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type ConnectionMessage struct {
|
|
9
|
+
Event string `json:"event"`
|
|
10
|
+
Body ConnectionMessageBody `json:"body"`
|
|
11
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package websocket
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// IncomingMessage represents any incoming message sent by Hookdeck.
|
|
9
|
+
type IncomingMessage struct {
|
|
10
|
+
*Attempt
|
|
11
|
+
// *RequestLogEvent
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// UnmarshalJSON deserializes incoming messages sent by Hookdeck into the
|
|
15
|
+
// appropriate structure.
|
|
16
|
+
func (m *IncomingMessage) UnmarshalJSON(data []byte) error {
|
|
17
|
+
incomingMessageEventOnly := struct {
|
|
18
|
+
Event string `json:"event"`
|
|
19
|
+
}{}
|
|
20
|
+
|
|
21
|
+
if err := json.Unmarshal(data, &incomingMessageEventOnly); err != nil {
|
|
22
|
+
return err
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
switch incomingMessageEventOnly.Event {
|
|
26
|
+
case "attempt":
|
|
27
|
+
var evt Attempt
|
|
28
|
+
if err := json.Unmarshal(data, &evt); err != nil {
|
|
29
|
+
return err
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
m.Attempt = &evt
|
|
33
|
+
case "connect_response":
|
|
34
|
+
return nil
|
|
35
|
+
default:
|
|
36
|
+
return fmt.Errorf("Unexpected message type: %s", incomingMessageEventOnly.Event)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return nil
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// MarshalJSON serializes outgoing messages sent to Hookdeck.
|
|
43
|
+
func (m OutgoingMessage) MarshalJSON() ([]byte, error) {
|
|
44
|
+
if m.AttemptResponse != nil {
|
|
45
|
+
return json.Marshal(m.AttemptResponse)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if m.ErrorAttemptResponse != nil {
|
|
49
|
+
return json.Marshal(m.ErrorAttemptResponse)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if m.ConnectionMessage != nil {
|
|
53
|
+
return json.Marshal(m.ConnectionMessage)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return json.Marshal(nil)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// OutgoingMessage represents any outgoing message sent to Hookdeck.
|
|
60
|
+
type OutgoingMessage struct {
|
|
61
|
+
*ErrorAttemptResponse
|
|
62
|
+
*AttemptResponse
|
|
63
|
+
*ConnectionMessage
|
|
64
|
+
}
|