Skip to content

Commit

Permalink
feat(apiclient): automatic IDN conversion of API command parameters t…
Browse files Browse the repository at this point in the history
…o punycode
  • Loading branch information
KaiSchwarz-cnic committed Apr 29, 2020
1 parent 8e98f04 commit 6562f26
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 26 deletions.
11 changes: 11 additions & 0 deletions README.md
Expand Up @@ -18,6 +18,17 @@ This module is a connector library for the insanely fast HEXONET Backend API. Fo
* [Release Notes](https://github.com/hexonet/python-sdk/releases)
* [Development Guide](https://hexonet-python-sdk.readthedocs.io/en/latest/developmentguide.html)

## Features

* Automatic IDN Domain name conversion to punycode (our API accepts only punycode format in commands)
* Allow nested associative arrays in API commands to improve for bulk parameters
* Connecting and communication with our API
* Several ways to access and deal with response data
* Getting the command again returned together with the response
* sessionless communication
* session-based communication
* possibility to save API session identifier in PHP session

## How to use this module in your project

All you need to know, can be found [here](https://hexonet-python-sdk.readthedocs.io/en/latest/#usage-guide).
Expand Down
73 changes: 58 additions & 15 deletions hexonet/apiconnector/apiclient.py
Expand Up @@ -58,13 +58,7 @@ def getPOSTData(self, cmd):
if not isinstance(cmd, str):
for key in sorted(cmd.keys()):
if (cmd[key] is not None):
if isinstance(cmd[key], list):
i = 0
while i < len(cmd[key]):
tmp += ("{0}{1}={2}\n").format(key, i, re.sub('[\r\n]', '', str(cmd[key][i])))
i += 1
else:
tmp += ("{0}={1}\n").format(key, re.sub('[\r\n]', '', str(cmd[key])))
tmp += ("{0}={1}\n").format(key, re.sub('[\r\n]', '', str(cmd[key])))
return ("{0}{1}={2}").format(data, quote('s_command'), quote(re.sub('\n$', '', tmp)))

def getSession(self):
Expand Down Expand Up @@ -213,7 +207,13 @@ def request(self, cmd):
"""
Perform API request using the given command
"""
data = self.getPOSTData(cmd).encode('UTF-8')
# flatten nested api command bulk parameters
newcmd = self.__flattenCommand(cmd)
# auto convert umlaut names to punycode
newcmd = self.__autoIDNConvert(newcmd)

# request command to API
data = self.getPOSTData(newcmd).encode('UTF-8')
# TODO: 300s (to be sure to get an API response)
try:
req = Request(self.__socketURL, data, {
Expand All @@ -226,19 +226,19 @@ def request(self, cmd):
body = rtm.getTemplate("httperror").getPlain()
if (self.__debugMode):
print((self.__socketURL, data, "HTTP communication failed", body, '\n', '\n'))
return Response(body, cmd)
return Response(body, newcmd)

def requestNextResponsePage(self, rr):
"""
Request the next page of list entries for the current list query
Useful for tables
"""
mycmd = self.__toUpperCaseKeys(rr.getCommand())
mycmd = rr.getCommand()
if ("LAST" in mycmd):
raise Exception("Parameter LAST in use. Please remove it to avoid issues in requestNextPage.")
first = 0
if ("FIRST" in mycmd):
first = mycmd["FIRST"]
first = int(mycmd["FIRST"])
total = rr.getRecordsTotalCount()
limit = rr.getRecordsLimitation()
first += limit
Expand Down Expand Up @@ -293,11 +293,54 @@ def useLIVESystem(self):
self.__socketConfig.setSystemEntity("54cd")
return self

def __toUpperCaseKeys(self, cmd):
def __flattenCommand(self, cmd):
"""
Translate all command parameter names to uppercase
Flatten API command to handle it easier later on (nested array for bulk params)
"""
newcmd = {}
for k in list(cmd.keys()):
newcmd[k.upper()] = cmd[k]
for key in list(cmd.keys()):
newKey = key.upper()
val = cmd[key]
if val is None:
continue
if isinstance(val, list):
i = 0
while i < len(val):
newcmd[newKey + str(i)] = re.sub(r'[\r\n]', '', str(val[i]))
i += 1
else:
newcmd[newKey] = re.sub(r'[\r\n]', '', str(val))
return newcmd

def __autoIDNConvert(self, cmd):
"""
Auto convert API command parameters to punycode, if necessary.
"""
# don't convert for convertidn command to avoid endless loop
# and ignore commands in string format(even deprecated)
if isinstance(cmd, str) or re.match(r'^CONVERTIDN$', cmd["COMMAND"], re.IGNORECASE):
return cmd

toconvert = []
keys = []
for key in cmd:
if re.match(r'^(DOMAIN|NAMESERVER|DNSZONE)([0-9]*)$', key, re.IGNORECASE):
keys.append(key)
if not keys.count:
return cmd
idxs = []
for key in keys:
if not re.match(r'^[a-z0-9.-]+$', cmd[key], re.IGNORECASE):
toconvert.append(cmd[key])
idxs.append(key)

r = self.request({
"COMMAND": "ConvertIDN",
"DOMAIN": toconvert
})
if r.isSuccess():
col = r.getColumn("ACE")
if col is not None:
for idx, pc in enumerate(col.getData()):
cmd[idxs[idx]] = pc
return cmd
47 changes: 36 additions & 11 deletions tests/test_apiclient.py
Expand Up @@ -88,17 +88,6 @@ def test_apiclientmethods():
})
assert enc == validate

# support bulk parameters also as nested array
validate = 's_entity=54cd&s_command=COMMAND%3DQueryDomainOptions%0ADOMAIN0%3Dexample1.com%0ADOMAIN1%3Dexample2.com'
enc = cl.getPOSTData({
"COMMAND": 'QueryDomainOptions',
"DOMAIN": [
'example1.com',
'example2.com'
]
})
assert enc == validate

# #.enableDebugMode()
cl.enableDebugMode()
cl.disableDebugMode()
Expand Down Expand Up @@ -264,6 +253,42 @@ def test_apiclientmethods():
assert rec is not None
assert rec.getDataByKey('SESSION') is not None

# support bulk parameters also as nested array (flattenCommand)
r = cl.request({
'COMMAND': 'CheckDomains',
'DOMAIN': ['example.com', 'example.net']
})
assert isinstance(r, R) is True
assert r.isSuccess() is True
assert r.getCode() is 200
assert r.getDescription() == "Command completed successfully"
cmd = r.getCommand()
keys = cmd.keys()
assert ("DOMAIN0" in keys) is True
assert ("DOMAIN1" in keys) is True
assert ("DOMAIN" in keys) is False
assert cmd["DOMAIN0"] == "example.com"
assert cmd["DOMAIN1"] == "example.net"

# support autoIDNConvert
r = cl.request({
'COMMAND': 'CheckDomains',
'DOMAIN': ['example.com', 'dömäin.example', 'example.net']
})
assert isinstance(r, R) is True
assert r.isSuccess() is True
assert r.getCode() is 200
assert r.getDescription() == "Command completed successfully"
cmd = r.getCommand()
keys = cmd.keys()
assert ("DOMAIN0" in keys) is True
assert ("DOMAIN1" in keys) is True
assert ("DOMAIN2" in keys) is True
assert ("DOMAIN" in keys) is False
assert cmd["DOMAIN0"] == "example.com"
assert cmd["DOMAIN1"] == "xn--dmin-moa0i.example"
assert cmd["DOMAIN2"] == "example.net"

# [login succeeded; role used]
cl.useOTESystem()
cl.setRoleCredentials('test.user', 'testrole', 'test.passw0rd')
Expand Down

0 comments on commit 6562f26

Please sign in to comment.