From 6562f26c8741efa54bcf293331b8a6687b198828 Mon Sep 17 00:00:00 2001 From: Kai Schwarz Date: Wed, 29 Apr 2020 16:03:09 +0200 Subject: [PATCH] feat(apiclient): automatic IDN conversion of API command parameters to punycode --- README.md | 11 +++++ hexonet/apiconnector/apiclient.py | 73 ++++++++++++++++++++++++------- tests/test_apiclient.py | 47 +++++++++++++++----- 3 files changed, 105 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 84b8e94..b9e1efc 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/hexonet/apiconnector/apiclient.py b/hexonet/apiconnector/apiclient.py index 43a564d..f3a45d0 100644 --- a/hexonet/apiconnector/apiclient.py +++ b/hexonet/apiconnector/apiclient.py @@ -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): @@ -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, { @@ -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 @@ -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 diff --git a/tests/test_apiclient.py b/tests/test_apiclient.py index 9c7e50b..232dc3d 100644 --- a/tests/test_apiclient.py +++ b/tests/test_apiclient.py @@ -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() @@ -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')