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

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""" 

10 

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 

20 

21rtm = RTM() 

22 

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" 

26 

27 

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() 

45 

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 

52 

53 def setDefaultLogger(self): 

54 """ 

55 Set default logger to use 

56 """ 

57 self.__logger = Logger() 

58 return self 

59 

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 

69 

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 

77 

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 

87 

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 

95 

96 def enableDebugMode(self): 

97 """ 

98 Enable Debug Output to STDOUT 

99 """ 

100 self.__debugMode = True 

101 return self 

102 

103 def disableDebugMode(self): 

104 """ 

105 Disable Debug Output 

106 """ 

107 self.__debugMode = False 

108 return self 

109 

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 ) 

132 

133 def getSession(self): 

134 """ 

135 Get the API Session that is currently set 

136 """ 

137 return self.__socketConfig.getSession() 

138 

139 def getURL(self): 

140 """ 

141 Get the API connection url that is currently set 

142 """ 

143 return self.__socketURL 

144 

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 

162 

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 

184 

185 def getVersion(self): 

186 """ 

187 Get the current module version 

188 """ 

189 return "4.0.1" 

190 

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 

200 

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 

209 

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 

216 

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 

223 

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 

230 

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 

238 

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 

246 

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) 

254 

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 

265 

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 

279 

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 

292 

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) 

301 

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 

323 

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 

346 

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 

362 

363 def setUserView(self, uid): 

364 """ 

365 Set a data view to a given subuser 

366 """ 

367 self.__socketConfig.setUser(uid) 

368 return self 

369 

370 def resetUserView(self): 

371 """ 

372 Reset data view back from subuser to user 

373 """ 

374 self.__socketConfig.setUser(None) 

375 return self 

376 

377 def useHighPerformanceConnectionSetup(self): 

378 """ 

379 Activate High Performance Setup 

380 """ 

381 self.setURL(ISPAPI_CONNECTION_URL_PROXY) 

382 return self 

383 

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 

390 

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 

398 

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 

406 

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 

425 

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 

436 

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 

449 

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