Coverage for hexonet/apiconnector/apiclient.py: 96%
246 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-08 15:49 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-08 15:49 +0000
1# -*- coding: utf-8 -*-
2"""
3 hexonet.apiconnector.apiclient
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 This module covers all necessary functionality for http communicatiton
6 with our Backend System.
7 :copyright: © 2018 by HEXONET GmbH.
8 :license: MIT, see LICENSE for more details.
9"""
11from hexonet.apiconnector.logger import Logger
12from hexonet.apiconnector.response import Response
13from hexonet.apiconnector.responsetemplatemanager import ResponseTemplateManager as RTM
14from hexonet.apiconnector.socketconfig import SocketConfig
15from urllib.parse import quote, unquote, urlparse, urlencode
16from urllib.request import urlopen, Request
17import re
18import copy
19import platform
21rtm = RTM()
23ISPAPI_CONNECTION_URL_PROXY = "http://127.0.0.1/api/call.cgi"
24ISPAPI_CONNECTION_URL_LIVE = "https://api.ispapi.net/api/call.cgi"
25ISPAPI_CONNECTION_URL_OTE = "https://api-ote.ispapi.net/api/call.cgi"
28class APIClient(object):
29 def __init__(self):
30 # API connection url
31 self.setURL(ISPAPI_CONNECTION_URL_LIVE)
32 # Object covering API connection data
33 self.__socketConfig = SocketConfig()
34 # activity flag for debug mode
35 self.__debugMode = False
36 # API connection timeout setting
37 self.__socketTimeout = 300000
38 self.useLIVESystem()
39 # user agent setting
40 self.__ua = ""
41 # additional connection settings
42 self.__curlopts = {}
43 # logger class instance
44 self.setDefaultLogger()
46 def setCustomLogger(self, logger):
47 """
48 Set custom logger to use instead of the default one
49 """
50 self.__logger = logger
51 return self
53 def setDefaultLogger(self):
54 """
55 Set default logger to use
56 """
57 self.__logger = Logger()
58 return self
60 def setProxy(self, proxy):
61 """
62 Set Proxy to use for API communication
63 """
64 if proxy == "":
65 self.__curlopts.pop("PROXY", None)
66 else:
67 self.__curlopts["PROXY"] = proxy
68 return self
70 def getProxy(self):
71 """
72 Get Proxy configuration value for API communication
73 """
74 if "PROXY" in self.__curlopts:
75 return self.__curlopts["PROXY"]
76 return None
78 def setReferer(self, referer):
79 """
80 Set the Referer Header to use for API communication
81 """
82 if referer == "":
83 self.__curlopts.pop("REFERER", None)
84 else:
85 self.__curlopts["REFERER"] = referer
86 return self
88 def getReferer(self):
89 """
90 Get the Referer Header configuration value
91 """
92 if "REFERER" in self.__curlopts:
93 return self.__curlopts["REFERER"]
94 return None
96 def enableDebugMode(self):
97 """
98 Enable Debug Output to STDOUT
99 """
100 self.__debugMode = True
101 return self
103 def disableDebugMode(self):
104 """
105 Disable Debug Output
106 """
107 self.__debugMode = False
108 return self
110 def getPOSTData(self, cmd, secured=False):
111 """
112 Serialize given command for POST request including connection configuration data
113 """
114 data = self.__socketConfig.getPOSTData()
115 if secured:
116 data = re.sub(r"s_pw=[^&]+", "s_pw=***", data)
117 tmp = ""
118 if not isinstance(cmd, str):
119 for key in sorted(cmd.keys()):
120 if cmd[key] is not None:
121 tmp += ("{0}={1}\n").format(
122 key, re.sub("[\r\n]", "", str(cmd[key]))
123 )
124 else:
125 tmp = cmd
126 tmp = tmp.rstrip("\n")
127 if secured:
128 tmp = re.sub(r"PASSWORD=[^\n]+", "PASSWORD=***", tmp)
129 return ("{0}{1}={2}").format(
130 data, quote("s_command"), quote(re.sub("\n$", "", tmp))
131 )
133 def getSession(self):
134 """
135 Get the API Session that is currently set
136 """
137 return self.__socketConfig.getSession()
139 def getURL(self):
140 """
141 Get the API connection url that is currently set
142 """
143 return self.__socketURL
145 def getUserAgent(self):
146 """
147 Get the User Agent
148 """
149 if len(self.__ua) == 0:
150 pid = "PYTHON-SDK"
151 pyv = platform.python_version()
152 pf = platform.system()
153 arch = platform.architecture()[0]
154 self.__ua = "%s (%s; %s; rv:%s) python/%s" % (
155 pid,
156 pf,
157 arch,
158 self.getVersion(),
159 pyv,
160 )
161 return self.__ua
163 def setUserAgent(self, pid, rv, modules=[]):
164 """
165 Possibility to customize default user agent to fit your needs by given string and revision
166 """
167 s = " "
168 mods = ""
169 if len(modules) > 0:
170 mods += " " + s.join(modules)
171 pyv = platform.python_version()
172 pf = platform.system()
173 arch = platform.architecture()[0]
174 self.__ua = "%s (%s; %s; rv:%s)%s python-sdk/%s python/%s" % (
175 pid,
176 pf,
177 arch,
178 rv,
179 mods,
180 self.getVersion(),
181 pyv,
182 )
183 return self
185 def getVersion(self):
186 """
187 Get the current module version
188 """
189 return "4.0.1"
191 def saveSession(self, session):
192 """
193 Apply session data (session id and system entity) to given client request session
194 """
195 session["socketcfg"] = {
196 "entity": self.__socketConfig.getSystemEntity(),
197 "session": self.__socketConfig.getSession(),
198 }
199 return self
201 def reuseSession(self, session):
202 """
203 Use existing configuration out of session
204 to rebuild and reuse connection settings
205 """
206 self.__socketConfig.setSystemEntity(session["socketcfg"]["entity"])
207 self.setSession(session["socketcfg"]["session"])
208 return self
210 def setURL(self, value):
211 """
212 Set another connection url to be used for API communication
213 """
214 self.__socketURL = value
215 return self
217 def setOTP(self, value):
218 """
219 Set one time password to be used for API communication
220 """
221 self.__socketConfig.setOTP(value)
222 return self
224 def setSession(self, value):
225 """
226 Set an API session id to be used for API communication
227 """
228 self.__socketConfig.setSession(value)
229 return self
231 def setRemoteIPAddress(self, value):
232 """
233 Set an Remote IP Address to be used for API communication.
234 To be used in case you have an active ip filter setting.
235 """
236 self.__socketConfig.setRemoteAddress(value)
237 return self
239 def setCredentials(self, uid, pw):
240 """
241 Set Credentials to be used for API communication
242 """
243 self.__socketConfig.setLogin(uid)
244 self.__socketConfig.setPassword(pw)
245 return self
247 def setRoleCredentials(self, uid, role, pw):
248 """
249 Set Credentials to be used for API communication
250 """
251 if role == "":
252 return self.setCredentials(uid, pw)
253 return self.setCredentials(("{0}!{1}").format(uid, role), pw)
255 def login(self, otp=""):
256 """
257 Perform API login to start session-based communication
258 """
259 self.setOTP(otp)
260 rr = self.request({"COMMAND": "StartSession"})
261 if rr.isSuccess():
262 col = rr.getColumn("SESSION")
263 self.setSession(col.getData()[0] if (col is not None) else None)
264 return rr
266 def loginExtended(self, params, otp=""):
267 """
268 Perform API login to start session-based communication.
269 Use given specific command parameters.
270 """
271 self.setOTP(otp)
272 cmd = {"COMMAND": "StartSession"}
273 cmd.update(params)
274 rr = self.request(cmd)
275 if rr.isSuccess():
276 col = rr.getColumn("SESSION")
277 self.setSession(col.getData()[0] if (col is not None) else None)
278 return rr
280 def logout(self):
281 """
282 Perform API logout to close API session in use
283 """
284 rr = self.request(
285 {
286 "COMMAND": "EndSession",
287 }
288 )
289 if rr.isSuccess():
290 self.setSession(None)
291 return rr
293 def request(self, cmd):
294 """
295 Perform API request using the given command
296 """
297 # flatten nested api command bulk parameters
298 newcmd = self.__flattenCommand(cmd)
299 # auto convert umlaut names to punycode
300 newcmd = self.__autoIDNConvert(newcmd)
302 # request command to API
303 cfg = {"CONNECTION_URL": self.__socketURL}
304 data = self.getPOSTData(newcmd).encode("UTF-8")
305 secured = self.getPOSTData(newcmd, True).encode("UTF-8")
306 error = None
307 try:
308 headers = {"User-Agent": self.getUserAgent()}
309 if "REFERER" in self.__curlopts:
310 headers["Referer"] = self.__curlopts["REFERER"]
311 req = Request(cfg["CONNECTION_URL"], data, headers)
312 if "PROXY" in self.__curlopts:
313 proxyurl = urlparse(self.__curlopts["PROXY"])
314 req.set_proxy(proxyurl.netloc, proxyurl.scheme)
315 body = urlopen(req, timeout=self.__socketTimeout).read()
316 except Exception as e:
317 error = str(e)
318 body = rtm.getTemplate("httperror").getPlain()
319 r = Response(body, newcmd, cfg)
320 if self.__debugMode:
321 self.__logger.log(secured, r, error)
322 return r
324 def requestNextResponsePage(self, rr):
325 """
326 Request the next page of list entries for the current list query
327 Useful for tables
328 """
329 mycmd = rr.getCommand()
330 if "LAST" in mycmd:
331 raise Exception(
332 "Parameter LAST in use. Please remove it to avoid issues in requestNextPage."
333 )
334 first = 0
335 if "FIRST" in mycmd:
336 first = int(mycmd["FIRST"])
337 total = rr.getRecordsTotalCount()
338 limit = rr.getRecordsLimitation()
339 first += limit
340 if first < total:
341 mycmd["FIRST"] = first
342 mycmd["LIMIT"] = limit
343 return self.request(mycmd)
344 else:
345 return None
347 def requestAllResponsePages(self, cmd):
348 """
349 Request all pages/entries for the given query command
350 """
351 responses = []
352 mycmd = copy.deepcopy(cmd)
353 mycmd["FIRST"] = 0
354 rr = self.request(mycmd)
355 tmp = rr
356 while tmp is not None:
357 responses.append(tmp)
358 tmp = self.requestNextResponsePage(tmp)
359 if tmp is None:
360 break
361 return responses
363 def setUserView(self, uid):
364 """
365 Set a data view to a given subuser
366 """
367 self.__socketConfig.setUser(uid)
368 return self
370 def resetUserView(self):
371 """
372 Reset data view back from subuser to user
373 """
374 self.__socketConfig.setUser(None)
375 return self
377 def useHighPerformanceConnectionSetup(self):
378 """
379 Activate High Performance Setup
380 """
381 self.setURL(ISPAPI_CONNECTION_URL_PROXY)
382 return self
384 def useDefaultConnectionSetup(self):
385 """
386 Activate Default Connection Setup (which is the default anyways)
387 """
388 self.setURL(ISPAPI_CONNECTION_URL_LIVE)
389 return self
391 def useOTESystem(self):
392 """
393 Set OT&E System for API communication
394 """
395 self.setURL(ISPAPI_CONNECTION_URL_OTE)
396 self.__socketConfig.setSystemEntity("1234")
397 return self
399 def useLIVESystem(self):
400 """
401 Set LIVE System for API communication (this is the default setting)
402 """
403 self.setURL(ISPAPI_CONNECTION_URL_LIVE)
404 self.__socketConfig.setSystemEntity("54cd")
405 return self
407 def __flattenCommand(self, cmd):
408 """
409 Flatten API command to handle it easier later on (nested array for bulk params)
410 """
411 newcmd = {}
412 for key in list(cmd.keys()):
413 newKey = key.upper()
414 val = cmd[key]
415 if val is None:
416 continue
417 if isinstance(val, list):
418 i = 0
419 while i < len(val):
420 newcmd[newKey + str(i)] = re.sub(r"[\r\n]", "", str(val[i]))
421 i += 1
422 else:
423 newcmd[newKey] = re.sub(r"[\r\n]", "", str(val))
424 return newcmd
426 def __autoIDNConvert(self, cmd):
427 """
428 Auto convert API command parameters to punycode, if necessary.
429 """
430 # don't convert for convertidn command to avoid endless loop
431 # and ignore commands in string format(even deprecated)
432 if isinstance(cmd, str) or re.match(
433 r"^CONVERTIDN$", cmd["COMMAND"], re.IGNORECASE
434 ):
435 return cmd
437 toconvert = []
438 keys = []
439 for key in cmd:
440 if re.match(r"^(DOMAIN|NAMESERVER|DNSZONE)([0-9]*)$", key, re.IGNORECASE):
441 keys.append(key)
442 idxs = []
443 for key in keys:
444 if not re.match(r"^[a-z0-9.-]+$", cmd[key], re.IGNORECASE):
445 toconvert.append(cmd[key])
446 idxs.append(key)
447 if not toconvert:
448 return cmd
450 r = self.request({"COMMAND": "ConvertIDN", "DOMAIN": toconvert})
451 if r.isSuccess():
452 col = r.getColumn("ACE")
453 if col is not None:
454 for idx, pc in enumerate(col.getData()):
455 cmd[idxs[idx]] = pc
456 return cmd