Passed
Push — main ( e52106...ac3387 )
by Acho
01:50
created

client.go   A

Size/Duplication

Total Lines 226
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 32
eloc 138
dl 0
loc 226
rs 9.84
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A smobilpay.*Client.getBaseHmacAuthString 0 6 1
A smobilpay.*Client.newRequest 0 24 4
A smobilpay.New 0 17 2
A smobilpay.*Client.Ping 0 17 4
A smobilpay.*Client.authPayload 0 11 3
A smobilpay.*Client.makeRequestConfig 0 8 2
A smobilpay.*Client.computeHmac 0 5 1
B smobilpay.*Client.do 0 23 6
A smobilpay.*Client.getAuthHeader 0 7 1
A smobilpay.*Client.newResponse 0 15 3
B smobilpay.*Client.getPayloadHmacAuthString 0 27 5
1
package smobilpay
2
3
import (
4
	"bytes"
5
	"context"
6
	"crypto/hmac"
7
	"crypto/sha1"
8
	"encoding/base64"
9
	"encoding/json"
10
	"fmt"
11
	"io"
12
	"io/ioutil"
13
	"net/http"
14
	"net/url"
15
	"sort"
16
	"strings"
17
)
18
19
type service struct {
20
	client *Client
21
}
22
23
// Client is the smobilpay API client.
24
// Do not instantiate this client with Client{}. Use the New method instead.
25
type Client struct {
26
	httpClient   *http.Client
27
	common       service
28
	baseURL      string
29
	accessToken  string
30
	accessSecret string
31
32
	Topup *topupService
33
}
34
35
// New creates and returns a new *Client from a slice of Option.
36
func New(options ...Option) *Client {
37
	config := defaultClientConfig()
38
39
	for _, option := range options {
40
		option.apply(config)
41
	}
42
43
	client := &Client{
44
		httpClient:   config.httpClient,
45
		accessToken:  config.accessToken,
46
		accessSecret: config.accessSecret,
47
		baseURL:      config.baseURL,
48
	}
49
50
	client.common.client = client
51
	client.Topup = (*topupService)(&client.common)
52
	return client
53
}
54
55
// Ping checks if the API is available
56
//
57
// https://apidocs.smobilpay.com/s3papi/API-Reference.2066448558.html
58
func (client *Client) Ping(ctx context.Context, options ...RequestOption) (*PingStatus, *Response, error) {
59
	request, err := client.newRequest(ctx, options, http.MethodGet, "/ping", nil)
60
	if err != nil {
61
		return nil, nil, err
62
	}
63
64
	response, err := client.do(request)
65
	if err != nil {
66
		return nil, response, err
67
	}
68
69
	status := new(PingStatus)
70
	if err = json.Unmarshal(*response.Body, status); err != nil {
71
		return nil, response, err
72
	}
73
74
	return status, response, nil
75
}
76
77
func (client *Client) makeRequestConfig(options []RequestOption) *requestConfig {
78
	config := defaultRequestConfig()
79
80
	for _, option := range options {
81
		option.apply(config)
82
	}
83
84
	return config
85
}
86
87
// newRequest creates an API request. A relative URL can be provided in uri,
88
// in which case it is resolved relative to the BaseURL of the Client.
89
// URI's should always be specified without a preceding slash.
90
func (client *Client) newRequest(ctx context.Context, options []RequestOption, method, uri string, body map[string]string) (*http.Request, error) {
91
	config := client.makeRequestConfig(options)
92
93
	var buf io.ReadWriter
94
	if body != nil {
95
		buf = &bytes.Buffer{}
96
		enc := json.NewEncoder(buf)
97
		enc.SetEscapeHTML(false)
98
		err := enc.Encode(body)
99
		if err != nil {
100
			return nil, err
101
		}
102
	}
103
104
	req, err := http.NewRequestWithContext(ctx, method, client.baseURL+uri, buf)
105
	if err != nil {
106
		return nil, err
107
	}
108
109
	req.Header.Set("Content-Type", "application/json")
110
	req.Header.Set("Accept", "application/json")
111
	req.Header.Set("Authorization", client.getAuthHeader(req, config, client.authPayload(req, body)))
112
113
	return req, nil
114
}
115
116
func (client *Client) authPayload(request *http.Request, body map[string]string) map[string]string {
117
	payload := map[string]string{}
118
	for key, value := range request.URL.Query() {
119
		payload[key] = value[0]
120
	}
121
122
	for key, value := range body {
123
		payload[key] = value
124
	}
125
126
	return payload
127
}
128
129
func (client *Client) getBaseHmacAuthString(request *http.Request) string {
130
	return fmt.Sprintf(
131
		"%s&%s",
132
		strings.ToUpper(request.Method),
133
		url.QueryEscape(
134
			fmt.Sprintf("%s://%s%s", request.URL.Scheme, request.URL.Host, request.URL.Path),
135
		),
136
	)
137
}
138
139
func (client *Client) getPayloadHmacAuthString(config *requestConfig, payload map[string]string) string {
140
	params := url.Values{}
141
	params.Add("s3pAuth_nonce", config.nonce)
142
	params.Add("s3pAuth_signature_method", "HMAC-SHA1")
143
	params.Add("s3pAuth_timestamp", config.timestampString())
144
	params.Add("s3pAuth_token", client.accessToken)
145
	for key, value := range payload {
146
		params.Add(key, strings.TrimSpace(value))
147
	}
148
149
	keys := make([]string, 0, len(params))
150
	for k := range params {
151
		keys = append(keys, k)
152
	}
153
	sort.Strings(keys)
154
155
	var buf strings.Builder
156
	for _, k := range keys {
157
		if buf.Len() > 0 {
158
			buf.WriteByte('&')
159
		}
160
		buf.WriteString(k)
161
		buf.WriteByte('=')
162
		buf.WriteString(params[k][0])
163
	}
164
165
	return buf.String()
166
}
167
168
func (client *Client) getAuthHeader(request *http.Request, config *requestConfig, payload map[string]string) string {
169
	return fmt.Sprintf(
170
		"s3pAuth,s3pAuth_nonce=\"%s\",s3pAuth_signature=\"%s\",s3pAuth_signature_method=\"HMAC-SHA1\",s3pAuth_timestamp=\"%s\",s3pAuth_token=\"%s\"",
171
		config.nonce,
172
		client.computeHmac(client.getBaseHmacAuthString(request)+"&"+url.QueryEscape(client.getPayloadHmacAuthString(config, payload))),
173
		config.timestampString(),
174
		client.accessToken,
175
	)
176
}
177
178
func (client *Client) computeHmac(message string) string {
179
	key := []byte(client.accessSecret)
180
	h := hmac.New(sha1.New, key)
181
	_, _ = h.Write([]byte(message))
182
	return base64.StdEncoding.EncodeToString(h.Sum(nil))
183
}
184
185
// do carries out an HTTP request and returns a Response
186
func (client *Client) do(req *http.Request) (*Response, error) {
187
	if req == nil {
188
		return nil, fmt.Errorf("%T cannot be nil", req)
189
	}
190
191
	httpResponse, err := client.httpClient.Do(req)
192
	if err != nil {
193
		return nil, err
194
	}
195
196
	defer func() { _ = httpResponse.Body.Close() }()
197
198
	resp, err := client.newResponse(httpResponse)
199
	if err != nil {
200
		return resp, err
201
	}
202
203
	_, err = io.Copy(ioutil.Discard, httpResponse.Body)
204
	if err != nil {
205
		return resp, err
206
	}
207
208
	return resp, nil
209
}
210
211
// newResponse converts an *http.Response to *Response
212
func (client *Client) newResponse(httpResponse *http.Response) (*Response, error) {
213
	if httpResponse == nil {
214
		return nil, fmt.Errorf("%T cannot be nil", httpResponse)
215
	}
216
217
	resp := new(Response)
218
	resp.HTTPResponse = httpResponse
219
220
	buf, err := ioutil.ReadAll(resp.HTTPResponse.Body)
221
	if err != nil {
222
		return nil, err
223
	}
224
	resp.Body = &buf
225
226
	return resp, resp.Error()
227
}
228