-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
208 lines (171 loc) · 5.25 KB
/
client.go
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
// Package client provides support to access the Selcom Pay API service.
package client
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
)
const version = "v1"
// This provides a default client configuration and is set with reasonable
// defaults. Users can replace this client with application specific settings
// using the WithClient function at the time a Client is constructed.
var defaultClient = http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 15 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
// Logger represents a function that will be called to add information
// to the user's application logs.
type Logger func(context.Context, string, ...any)
// Client represents a client that can talk to the selcompay API service.
type Client struct {
log Logger
host string
apiKey string
apiSecret string
http *http.Client
}
// New constructs a client that can be used to talk to the selcompay api.
func New(logger Logger, host string, apiKey string, apiSecret string, options ...func(cln *Client)) *Client {
cln := Client{
log: logger,
host: strings.TrimLeft(host, "/"),
apiKey: apiKey,
apiSecret: apiSecret,
http: &defaultClient,
}
for _, option := range options {
option(&cln)
}
return &cln
}
// WithClient adds a custom client for processing requests. It's recommend
// to not use the default client and provide your own.
func WithClient(http *http.Client) func(cln *Client) {
return func(cln *Client) {
cln.http = http
}
}
func (cln *Client) do(ctx context.Context, method string, url string, body any, v any) error {
resp, err := do(ctx, cln, method, url, body)
if err != nil {
return err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if err := json.Unmarshal(data, &v); err != nil {
return fmt.Errorf("client: response: %s: unmarshaling: error: %w", string(data), err)
}
return nil
}
func do(ctx context.Context, cln *Client, method string, url string, body any) (*http.Response, error) {
cln.log(ctx, "do: rawRequest: started", "method", method, "endpoint", url)
defer func() {
cln.log(ctx, "do: rawRequest: completed", "status", url)
}()
var b bytes.Buffer
if body != nil {
if err := json.NewEncoder(&b).Encode(body); err != nil {
return nil, fmt.Errorf("encoding request body: error: %w", err)
}
}
req, err := http.NewRequestWithContext(ctx, method, url, &b)
if err != nil {
return nil, fmt.Errorf("create request error: %w", err)
}
// Set Auth Headers
if err := cln.setHeaders(req, &b); err != nil {
return nil, err
}
// Log the Headers (Testing)
cln.log(ctx, fmt.Sprintf("%v", req.Header))
resp, err := cln.http.Do(req)
if err != nil {
return nil, fmt.Errorf("do: error: %w", err)
}
if resp.StatusCode != http.StatusOK {
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("readall: error: %w", err)
}
var trustedErr Error
if err := json.Unmarshal(data, &trustedErr); err != nil {
return nil, fmt.Errorf("decoding: response: %s, error: %w ", string(data), err)
}
trustedErr.statuscode = resp.StatusCode
return nil, fmt.Errorf("error: response: %w", trustedErr)
}
return resp, nil
}
func (cln *Client) setHeaders(req *http.Request, data *bytes.Buffer) error {
// Set default header values
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
authToken := fmt.Sprintf("SELCOM %s", base64Encode([]byte(cln.apiKey)))
digestMethod := "HS256"
signedFields, digest, timestamp, err := constructHeaders(cln, data)
if err != nil {
return err
}
// Set the auth Headers
req.Header.Set("Authorization", authToken)
req.Header.Set("Digest-Method", digestMethod)
req.Header.Set("Digest", digest)
req.Header.Set("Timestamp", timestamp)
req.Header.Set("Signed-Fields", signedFields)
return nil
}
func base64Encode(token []byte) string {
return base64.StdEncoding.EncodeToString(token)
}
func constructHeaders(cln *Client, params *bytes.Buffer) (string, string, string, error) {
var (
signedFields string
digest string
)
timestamp := time.Now().Format(time.RFC3339)
var jsonData map[string]any
if err := json.Unmarshal(params.Bytes(), &jsonData); err != nil {
return signedFields, digest, timestamp, fmt.Errorf("unmarshal error: %w", err)
}
data := fmt.Sprintf("timestamp=%s", timestamp)
for k, v := range jsonData {
data = fmt.Sprintf("%s&%s=%v", data, k, v)
if signedFields == "" {
signedFields = k
} else {
signedFields = strings.Join([]string{signedFields, k}, ",")
}
}
// Generate the digest using the data.
mac := hmac.New(sha256.New, []byte(cln.apiSecret))
_, err := mac.Write([]byte(data))
if err != nil {
return signedFields, digest, timestamp, fmt.Errorf("create signature: error: %w", err)
}
digest = base64Encode(mac.Sum(nil))
return signedFields, digest, timestamp, nil
}