Passed
Push — main ( 371a89...53658a )
by Acho
01:48
created

smobilpay.*Client.getPayloadHmacAuthString   A

Complexity

Conditions 2

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 9
nop 2
dl 0
loc 12
rs 9.95
c 0
b 0
f 0
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) urlEncode(params url.Values) string {
140
	keys := make([]string, 0, len(params))
141
	for k := range params {
142
		keys = append(keys, k)
143
	}
144
	sort.Strings(keys)
145
146
	var buf strings.Builder
147
	for _, k := range keys {
148
		if buf.Len() > 0 {
149
			buf.WriteByte('&')
150
		}
151
		buf.WriteString(k)
152
		buf.WriteByte('=')
153
		buf.WriteString(params[k][0])
154
	}
155
156
	return buf.String()
157
}
158
159
func (client *Client) getPayloadHmacAuthString(config *requestConfig, payload map[string]string) string {
160
	params := url.Values{}
161
	params.Add("s3pAuth_nonce", config.nonce)
162
	params.Add("s3pAuth_signature_method", "HMAC-SHA1")
163
	params.Add("s3pAuth_timestamp", config.timestampString())
164
	params.Add("s3pAuth_token", client.accessToken)
165
166
	for key, value := range payload {
167
		params.Add(key, strings.TrimSpace(value))
168
	}
169
170
	return client.urlEncode(params)
171
}
172
173
func (client *Client) getAuthHeader(request *http.Request, config *requestConfig, payload map[string]string) string {
174
	return fmt.Sprintf(
175
		"s3pAuth,s3pAuth_nonce=\"%s\",s3pAuth_signature=\"%s\",s3pAuth_signature_method=\"HMAC-SHA1\",s3pAuth_timestamp=\"%s\",s3pAuth_token=\"%s\"",
176
		config.nonce,
177
		client.computeHmac(client.getBaseHmacAuthString(request)+"&"+url.QueryEscape(client.getPayloadHmacAuthString(config, payload))),
178
		config.timestampString(),
179
		client.accessToken,
180
	)
181
}
182
183
func (client *Client) computeHmac(message string) string {
184
	key := []byte(client.accessSecret)
185
	h := hmac.New(sha1.New, key)
186
	_, _ = h.Write([]byte(message))
187
	return base64.StdEncoding.EncodeToString(h.Sum(nil))
188
}
189
190
// do carries out an HTTP request and returns a Response
191
func (client *Client) do(req *http.Request) (*Response, error) {
192
	if req == nil {
193
		return nil, fmt.Errorf("%T cannot be nil", req)
194
	}
195
196
	httpResponse, err := client.httpClient.Do(req)
197
	if err != nil {
198
		return nil, err
199
	}
200
201
	defer func() { _ = httpResponse.Body.Close() }()
202
203
	resp, err := client.newResponse(httpResponse)
204
	if err != nil {
205
		return resp, err
206
	}
207
208
	_, err = io.Copy(ioutil.Discard, httpResponse.Body)
209
	if err != nil {
210
		return resp, err
211
	}
212
213
	return resp, nil
214
}
215
216
// newResponse converts an *http.Response to *Response
217
func (client *Client) newResponse(httpResponse *http.Response) (*Response, error) {
218
	if httpResponse == nil {
219
		return nil, fmt.Errorf("%T cannot be nil", httpResponse)
220
	}
221
222
	resp := new(Response)
223
	resp.HTTPResponse = httpResponse
224
225
	buf, err := ioutil.ReadAll(resp.HTTPResponse.Body)
226
	if err != nil {
227
		return nil, err
228
	}
229
	resp.Body = &buf
230
231
	return resp, resp.Error()
232
}
233