-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.go
More file actions
273 lines (246 loc) · 6.61 KB
/
client.go
File metadata and controls
273 lines (246 loc) · 6.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
package api
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"golang.org/x/net/websocket"
)
const (
defaultHeaderToken = "Authorization"
defaultTokenPrefix = "Bearer"
)
// Client is a way to connect to 3rd party API servers.
type Client struct {
apiEndPoint string
apiToken string
headerToken string // What header should we use to send the token (eg, "Authorization")
tokenPrefix string // What to send before the token (eg, "Bearer", "Basic"...)
paramToken string // What query parameter should we use to send the token (eg, "private_token")
disallowUnknownFields bool
unixSocket string
websocketOrigin string
}
// NewClient creates a new Client ready to use.
func NewClient(apiEndPoint string) *Client {
return &Client{apiEndPoint: apiEndPoint}
}
// WithToken adds a token to a Client.
func (c *Client) WithToken(tk string) *Client {
c2 := new(Client)
*c2 = *c
c2.apiToken = tk
return c2
}
// WithHeaderToken specifies which Header line to use when sending a token.
func (c *Client) WithHeaderToken(ht string) *Client {
c2 := new(Client)
*c2 = *c
c2.headerToken = ht
return c2
}
// WithTokenPrefix adds an optional prefix to the token in the Header line.
func (c *Client) WithTokenPrefix(tp string) *Client {
c2 := new(Client)
*c2 = *c
c2.tokenPrefix = tp
return c2
}
// WithParamToken specifies which query parameter to use when sending a token.
func (c *Client) WithParamToken(pt string) *Client {
c2 := new(Client)
*c2 = *c
c2.paramToken = pt
return c2
}
// WithUserPass specifies which user and password to use with Basic authentication.
func (c *Client) WithUserPass(user, pass string) *Client {
c2 := new(Client)
*c2 = *c
if c2.tokenPrefix == "" {
c2.tokenPrefix = "Basic"
}
c2.apiToken = base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", user, pass)))
return c2
}
// DisallowUnknownFields causes the JSON decoder to return an error when the
// destination is a struct and the input contains object keys which do not
// match any non-ignored, exported fields in the destination.
func (c *Client) DisallowUnknownFields() *Client {
c2 := new(Client)
*c2 = *c
c2.disallowUnknownFields = true
return c2
}
// WithUnixSocket causes the client to connect through this Unix domain socket,
// instead of using the network.
func (c *Client) WithUnixSocket(socket string) *Client {
c2 := new(Client)
*c2 = *c
c2.unixSocket = socket
return c2
}
// WithWebsocketOrigin adds a custom "Origin" header in the websocket connections.
func (c *Client) WithWebsocketOrigin(origin string) *Client {
c2 := new(Client)
*c2 = *c
c2.websocketOrigin = origin
return c2
}
// Request makes a HTTP request to the API.
//
// If data is a []byte, it will be sent as-is; otherwise, it will be encoded using JSON.
//
// If dest is a pointer to a []byte, it will receive the output as-is; otherwise,
// the output will be JSON-decoded.
func (c *Client) Request(method, URL string, data any, dest any) error {
var err error
var body io.Reader
if data != nil {
var b []byte
switch d := data.(type) {
case []byte:
b = d
default:
b, err = json.Marshal(data)
if err != nil {
return err
}
}
body = bytes.NewBuffer(b)
}
header := make(http.Header)
u, err := c.urlAndHeader(URL, header)
if err != nil {
return err
}
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
return err
}
req.Header = header
client := &http.Client{}
if c.unixSocket != "" {
client.Transport = &http.Transport{
Dial: func(proto, addr string) (conn net.Conn, err error) {
return net.Dial("unix", c.unixSocket)
},
}
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("api: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
var foo struct {
Error string
}
decoder := json.NewDecoder(resp.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&foo); err != nil {
return fmt.Errorf("%s", resp.Status)
}
return fmt.Errorf("%s: %s", resp.Status, foo.Error)
}
if dest == nil {
return nil
}
if d, ok := dest.(*[]byte); ok {
b, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
*d = b
return nil
}
decoder := json.NewDecoder(resp.Body)
if c.disallowUnknownFields {
decoder.DisallowUnknownFields()
}
if err := decoder.Decode(dest); err != nil {
return err
}
return nil
}
// Get makes a HTTP GET request to the API.
func (c *Client) Get(URL string, dest any) error {
return c.Request("GET", URL, nil, dest)
}
// Post makes a HTTP POST request to the API.
func (c *Client) Post(URL string, data any, dest any) error {
return c.Request("POST", URL, data, dest)
}
// Put makes a HTTP PUT request to the API.
func (c *Client) Put(URL string, data any, dest any) error {
return c.Request("PUT", URL, data, dest)
}
// Delete makes a HTTP DELETE request to the API.
func (c *Client) Delete(URL string, dest any) error {
return c.Request("DELETE", URL, []byte(nil), dest)
}
// WS makes a websocket connection to the API.
// User must close the connection after it is no longer needed.
func (c *Client) WS(URL string) (*websocket.Conn, error) {
origin := "http://localhost/"
if c.websocketOrigin != "" {
origin = c.websocketOrigin
}
header := make(http.Header)
u, err := c.urlAndHeader(URL, header)
if err != nil {
return nil, err
}
switch u.Scheme {
case "http":
u.Scheme = "ws"
case "https":
u.Scheme = "wss"
}
config, err := websocket.NewConfig(u.String(), origin)
if err != nil {
return nil, err
}
config.Header = header
ws, err := websocket.DialConfig(config)
if err != nil {
return nil, err
}
return ws, nil
}
func (c *Client) urlAndHeader(URL string, header http.Header) (*url.URL, error) {
// make headerToken and tokenPrefix the default values if needed, but only for this call.
headerToken, tokenPrefix := c.headerToken, c.tokenPrefix
if c.apiToken != "" && headerToken == "" && c.paramToken == "" {
headerToken = defaultHeaderToken
if tokenPrefix == "" {
tokenPrefix = defaultTokenPrefix
}
}
// We use this instead of url.JoinPath because the latter removes possible query parameters
u, err := url.Parse(strings.TrimSuffix(c.apiEndPoint, "/") + "/" + strings.TrimPrefix(URL, "/"))
if err != nil {
return nil, err
}
if c.apiToken != "" && c.paramToken != "" {
v, err := url.ParseQuery(u.RawQuery)
if err != nil {
return nil, err
}
v.Add(c.paramToken, c.apiToken)
u.RawQuery = v.Encode()
}
if c.apiToken != "" && headerToken != "" {
token := c.apiToken
if tokenPrefix != "" {
token = tokenPrefix + " " + token
}
header.Set(headerToken, token)
}
return u, nil
}