Skip to content

Commit 407c105

Browse files
feat(apiclient): automatic IDN conversion of API command parameters to punycode
BREAKING CHANGE: Even though thought and build for internal purposes, we launch a major version for this change. type of cmd parameter changes from map[string]inteface{} to map[string]string.
1 parent 303b8b3 commit 407c105

File tree

3 files changed

+142
-27
lines changed

3 files changed

+142
-27
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ This module is a connector library for the insanely fast HEXONET Backend API. Fo
1717
* [Release Notes](https://github.com/hexonet/go-sdk/releases)
1818
* [Development Guide](https://github.com/hexonet/go-sdk/wiki/Development-Guide)
1919

20+
## Features
21+
22+
* Automatic IDN Domain name conversion to punycode (our API accepts only punycode format in commands)
23+
* Allow nested associative arrays in API commands to improve for bulk parameters
24+
* Connecting and communication with our API
25+
* Several ways to access and deal with response data
26+
* Getting the command again returned together with the response
27+
* sessionless communication
28+
* session-based communication
29+
* possibility to save API session identifier in session
30+
2031
## How to use this module in your project
2132

2233
We have also a demo app available showing how to integrate and use our SDK. See [here](https://github.com/hexonet/go-sdk-demo).

apiclient/apiclient.go

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"net/http"
1515
"net/url"
1616
"reflect"
17+
"regexp"
1718
"runtime"
1819
"sort"
1920
"strconv"
@@ -75,8 +76,7 @@ func (cl *APIClient) DisableDebugMode() *APIClient {
7576

7677
// GetPOSTData method to Serialize given command for POST request
7778
// including connection configuration data
78-
func (cl *APIClient) GetPOSTData(thecmd map[string]interface{}) string {
79-
cmd := cl.flattenCommand(thecmd)
79+
func (cl *APIClient) GetPOSTData(cmd map[string]string) string {
8080
data := cl.socketConfig.GetPOSTData()
8181
var tmp strings.Builder
8282
keys := []string{}
@@ -258,8 +258,13 @@ func (cl *APIClient) Logout() *R.Response {
258258

259259
// Request method to perform API request using the given command
260260
func (cl *APIClient) Request(cmd map[string]interface{}) *R.Response {
261+
// flatten nested api command bulk parameters
261262
newcmd := cl.flattenCommand(cmd)
262-
data := cl.GetPOSTData(cmd)
263+
// auto convert umlaut names to punycode
264+
newcmd = cl.autoIDNConvert(newcmd)
265+
266+
// request command to API
267+
data := cl.GetPOSTData(newcmd)
263268

264269
client := &http.Client{
265270
Timeout: cl.socketTimeout,
@@ -332,7 +337,7 @@ func (cl *APIClient) Request(cmd map[string]interface{}) *R.Response {
332337
// Useful for lists
333338
func (cl *APIClient) RequestNextResponsePage(rr *R.Response) (*R.Response, error) {
334339
mycmd := map[string]interface{}{}
335-
for key, val := range cl.toUpperCaseKeys(rr.GetCommand()) {
340+
for key, val := range rr.GetCommand() {
336341
mycmd[key] = val
337342
}
338343
if _, ok := mycmd["LAST"]; ok {
@@ -400,32 +405,80 @@ func (cl *APIClient) UseLIVESystem() *APIClient {
400405
return cl
401406
}
402407

403-
// toUpperCaseKeys method to translate all command parameter names to uppercase
404-
func (cl *APIClient) toUpperCaseKeys(cmd map[string]string) map[string]string {
405-
newcmd := map[string]string{}
406-
for k, v := range cmd {
407-
newcmd[strings.ToUpper(k)] = v
408-
}
409-
return newcmd
410-
}
411-
412408
// flattenCommand method to translate all command parameter names to uppercase
413409
func (cl *APIClient) flattenCommand(cmd map[string]interface{}) map[string]string {
414410
newcmd := map[string]string{}
415411
for key, val := range cmd {
412+
newKey := strings.ToUpper(key)
416413
if reflect.TypeOf(val).Kind() == reflect.Slice {
417414
v := val.([]string)
418415
for idx, str := range v {
419416
str = strings.Replace(str, "\r", "", -1)
420417
str = strings.Replace(str, "\n", "", -1)
421-
newcmd[key+strconv.Itoa(idx)] = str
418+
newcmd[newKey+strconv.Itoa(idx)] = str
422419
}
423420
} else {
424421
val := val.(string)
425422
val = strings.Replace(val, "\r", "", -1)
426423
val = strings.Replace(val, "\n", "", -1)
424+
newcmd[newKey] = val
425+
}
426+
}
427+
return newcmd
428+
}
429+
430+
// autoIDNConvert method to translate all whitelisted parameter values to punycode, if necessary
431+
func (cl *APIClient) autoIDNConvert(cmd map[string]string) map[string]string {
432+
newcmd := map[string]string{
433+
"COMMAND": "ConvertIDN",
434+
}
435+
// don't convert for convertidn command to avoid endless loop
436+
pattern := regexp.MustCompile(`(?i)^CONVERTIDN$`)
437+
mm := pattern.MatchString(cmd["COMMAND"])
438+
if mm {
439+
return cmd
440+
}
441+
keys := []string{}
442+
pattern = regexp.MustCompile(`(?i)^(DOMAIN|NAMESERVER|DNSZONE)([0-9]*)$`)
443+
for key := range cmd {
444+
mm = pattern.MatchString(key)
445+
if mm {
446+
keys = append(keys, key)
447+
}
448+
}
449+
if len(keys) == 0 {
450+
return cmd
451+
}
452+
toconvert := []string{}
453+
idxs := []string{}
454+
pattern = regexp.MustCompile(`\r|\n`)
455+
idnpattern := regexp.MustCompile(`(?i)[^a-z0-9. -]+`)
456+
for i := 0; i < len(keys); i++ {
457+
key := keys[i]
458+
val := pattern.ReplaceAllString(cmd[key], "")
459+
mm = idnpattern.MatchString(val)
460+
if mm {
461+
toconvert = append(toconvert, val)
462+
idxs = append(idxs, key)
463+
} else {
427464
newcmd[key] = val
428465
}
429466
}
467+
if len(toconvert) == 0 {
468+
return cmd
469+
}
470+
r := cl.Request(map[string]interface{}{
471+
"COMMAND": "ConvertIDN",
472+
"DOMAIN": toconvert,
473+
})
474+
if !r.IsSuccess() {
475+
return cmd
476+
}
477+
col := r.GetColumn("ACE")
478+
if col != nil {
479+
for idx, pc := range col.GetData() {
480+
newcmd[idxs[idx]] = pc
481+
}
482+
}
430483
return newcmd
431484
}

apiclient/apiclient_test.go

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"runtime"
7+
"strconv"
78
"strings"
89
"testing"
910

@@ -31,7 +32,7 @@ func TestMain(m *testing.M) {
3132

3233
func TestGetPOSTData1(t *testing.T) {
3334
validate := "s_entity=54cd&s_command=AUTH%3Dgwrgwqg%25%26%5C44t3%2A%0ACOMMAND%3DModifyDomain"
34-
enc := cl.GetPOSTData(map[string]interface{}{
35+
enc := cl.GetPOSTData(map[string]string{
3536
"COMMAND": "ModifyDomain",
3637
"AUTH": "gwrgwqg%&\\44t3*",
3738
})
@@ -48,6 +49,56 @@ func TestDisableDebugMode(t *testing.T) {
4849
cl.DisableDebugMode()
4950
}
5051

52+
func TestRequestFlattenCommand(t *testing.T) {
53+
cl.SetCredentials("test.user", "test.passw0rd")
54+
cl.UseOTESystem()
55+
r := cl.Request(map[string]interface{}{
56+
"COMMAND": "CheckDomains",
57+
"DOMAiN": []string{"example.com", "example.net"},
58+
})
59+
if !r.IsSuccess() || r.GetCode() != 200 || r.GetDescription() != "Command completed successfully" {
60+
t.Error("TestRequestFlattenCommand: Expected response to succeed.")
61+
}
62+
cmd := r.GetCommand()
63+
val1, exists1 := cmd["DOMAIN0"]
64+
val2, exists2 := cmd["DOMAIN1"]
65+
_, exists3 := cmd["DOMAIN"]
66+
_, exists4 := cmd["DOMAiN"]
67+
if !exists1 || !exists2 || exists3 || exists4 {
68+
t.Error("TestRequestFlattenCommand: DOMAIN parameter flattening not working (keys).")
69+
}
70+
if val1 != "example.com" || val2 != "example.net" {
71+
t.Error("TestRequestFlattenCommand: DOMAIN parameter flattening not working (vals).")
72+
}
73+
}
74+
75+
func TestAutoIDNConvertCommand(t *testing.T) {
76+
cl.SetCredentials("test.user", "test.passw0rd")
77+
cl.UseOTESystem()
78+
r := cl.Request(map[string]interface{}{
79+
"COMMAND": "CheckDomains",
80+
"DOMAiN": []string{"example.com", "dömäin.example", "example.net"},
81+
})
82+
if !r.IsSuccess() || r.GetCode() != 200 || r.GetDescription() != "Command completed successfully" {
83+
t.Error("TestRequestFlattenCommand: Expected response to succeed." + strconv.Itoa(r.GetCode()) + r.GetDescription())
84+
}
85+
cmd := r.GetCommand()
86+
val1, exists1 := cmd["DOMAIN0"]
87+
val2, exists2 := cmd["DOMAIN1"]
88+
val3, exists3 := cmd["DOMAIN2"]
89+
_, exists4 := cmd["DOMAIN"]
90+
_, exists5 := cmd["DOMAiN"]
91+
if !exists1 || !exists2 || !exists3 || exists4 || exists5 {
92+
t.Error("TestRequestFlattenCommand: DOMAIN parameter flattening not working (keys).")
93+
}
94+
if val1 != "example.com" || val2 != "xn--dmin-moa0i.example" || val3 != "example.net" {
95+
t.Error("TestRequestFlattenCommand: DOMAIN parameter flattening not working (vals).")
96+
}
97+
// reset to defaults for following tests
98+
cl.SetCredentials("", "")
99+
cl.UseLIVESystem()
100+
}
101+
51102
func TestGetSession1(t *testing.T) {
52103
cl.Logout()
53104
session, err := cl.GetSession()
@@ -105,7 +156,7 @@ func TestSetURL(t *testing.T) {
105156

106157
func TestSetOTP1(t *testing.T) {
107158
cl.SetOTP("12345678")
108-
tmp := cl.GetPOSTData(map[string]interface{}{
159+
tmp := cl.GetPOSTData(map[string]string{
109160
"COMMAND": "StatusAccount",
110161
})
111162
if strings.Compare(tmp, "s_entity=54cd&s_otp=12345678&s_command=COMMAND%3DStatusAccount") != 0 {
@@ -115,7 +166,7 @@ func TestSetOTP1(t *testing.T) {
115166

116167
func TestSetOTP2(t *testing.T) {
117168
cl.SetOTP("")
118-
tmp := cl.GetPOSTData(map[string]interface{}{
169+
tmp := cl.GetPOSTData(map[string]string{
119170
"COMMAND": "StatusAccount",
120171
})
121172
if strings.Compare(tmp, "s_entity=54cd&s_command=COMMAND%3DStatusAccount") != 0 {
@@ -125,7 +176,7 @@ func TestSetOTP2(t *testing.T) {
125176

126177
func TestSetSession1(t *testing.T) {
127178
cl.SetSession("12345678")
128-
tmp := cl.GetPOSTData(map[string]interface{}{
179+
tmp := cl.GetPOSTData(map[string]string{
129180
"COMMAND": "StatusAccount",
130181
})
131182
if strings.Compare(tmp, "s_entity=54cd&s_session=12345678&s_command=COMMAND%3DStatusAccount") != 0 {
@@ -137,7 +188,7 @@ func TestSetSession2(t *testing.T) {
137188
cl.SetRoleCredentials("myaccountid", "myrole", "mypassword")
138189
cl.SetOTP("12345678")
139190
cl.SetSession("12345678")
140-
tmp := cl.GetPOSTData(map[string]interface{}{
191+
tmp := cl.GetPOSTData(map[string]string{
141192
"COMMAND": "StatusAccount",
142193
})
143194
if strings.Compare(tmp, "s_entity=54cd&s_session=12345678&s_command=COMMAND%3DStatusAccount") != 0 {
@@ -147,7 +198,7 @@ func TestSetSession2(t *testing.T) {
147198

148199
func TestSetSession3(t *testing.T) {
149200
cl.SetSession("")
150-
tmp := cl.GetPOSTData(map[string]interface{}{
201+
tmp := cl.GetPOSTData(map[string]string{
151202
"COMMAND": "StatusAccount",
152203
})
153204
if strings.Compare(tmp, "s_entity=54cd&s_command=COMMAND%3DStatusAccount") != 0 {
@@ -161,7 +212,7 @@ func TestSaveReuseSession(t *testing.T) {
161212
cl.SaveSession(sessionobj)
162213
cl2 := NewAPIClient()
163214
cl2.ReuseSession(sessionobj)
164-
tmp := cl2.GetPOSTData(map[string]interface{}{
215+
tmp := cl2.GetPOSTData(map[string]string{
165216
"COMMAND": "StatusAccount",
166217
})
167218
if strings.Compare(tmp, "s_entity=54cd&s_session=12345678&s_command=COMMAND%3DStatusAccount") != 0 {
@@ -172,7 +223,7 @@ func TestSaveReuseSession(t *testing.T) {
172223

173224
func TestSetRemoteIPAddress1(t *testing.T) {
174225
cl.SetRemoteIPAddress("10.10.10.10")
175-
tmp := cl.GetPOSTData(map[string]interface{}{
226+
tmp := cl.GetPOSTData(map[string]string{
176227
"COMMAND": "StatusAccount",
177228
})
178229
if strings.Compare(tmp, "s_entity=54cd&s_remoteaddr=10.10.10.10&s_command=COMMAND%3DStatusAccount") != 0 {
@@ -182,7 +233,7 @@ func TestSetRemoteIPAddress1(t *testing.T) {
182233

183234
func TestSetRemoteIPAddress2(t *testing.T) {
184235
cl.SetRemoteIPAddress("")
185-
tmp := cl.GetPOSTData(map[string]interface{}{
236+
tmp := cl.GetPOSTData(map[string]string{
186237
"COMMAND": "StatusAccount",
187238
})
188239
if strings.Compare(tmp, "s_entity=54cd&s_command=COMMAND%3DStatusAccount") != 0 {
@@ -192,7 +243,7 @@ func TestSetRemoteIPAddress2(t *testing.T) {
192243

193244
func TestSetCredentials1(t *testing.T) {
194245
cl.SetCredentials("myaccountid", "mypassword")
195-
tmp := cl.GetPOSTData(map[string]interface{}{
246+
tmp := cl.GetPOSTData(map[string]string{
196247
"COMMAND": "StatusAccount",
197248
})
198249
if strings.Compare(tmp, "s_entity=54cd&s_login=myaccountid&s_pw=mypassword&s_command=COMMAND%3DStatusAccount") != 0 {
@@ -202,7 +253,7 @@ func TestSetCredentials1(t *testing.T) {
202253

203254
func TestSetCredentials2(t *testing.T) {
204255
cl.SetCredentials("", "")
205-
tmp := cl.GetPOSTData(map[string]interface{}{
256+
tmp := cl.GetPOSTData(map[string]string{
206257
"COMMAND": "StatusAccount",
207258
})
208259
if strings.Compare(tmp, "s_entity=54cd&s_command=COMMAND%3DStatusAccount") != 0 {
@@ -212,7 +263,7 @@ func TestSetCredentials2(t *testing.T) {
212263

213264
func TestSetRoleCredentials1(t *testing.T) {
214265
cl.SetRoleCredentials("myaccountid", "myroleid", "mypassword")
215-
tmp := cl.GetPOSTData(map[string]interface{}{
266+
tmp := cl.GetPOSTData(map[string]string{
216267
"COMMAND": "StatusAccount",
217268
})
218269
if strings.Compare(tmp, "s_entity=54cd&s_login=myaccountid%21myroleid&s_pw=mypassword&s_command=COMMAND%3DStatusAccount") != 0 {
@@ -222,7 +273,7 @@ func TestSetRoleCredentials1(t *testing.T) {
222273

223274
func TestSetRoleCredentials2(t *testing.T) {
224275
cl.SetRoleCredentials("", "", "")
225-
tmp := cl.GetPOSTData(map[string]interface{}{
276+
tmp := cl.GetPOSTData(map[string]string{
226277
"COMMAND": "StatusAccount",
227278
})
228279
if strings.Compare(tmp, "s_entity=54cd&s_command=COMMAND%3DStatusAccount") != 0 {

0 commit comments

Comments
 (0)