|
| 1 | +# -*- coding: UTF-8 -*- |
| 2 | + |
| 3 | +''' |
| 4 | +@author: Rafael Sanz |
| 5 | +@contact: rafael.sanz@selab.es |
| 6 | +@Copyright: 2016 <Rafael Sanz - (R)SELAB> |
| 7 | +
|
| 8 | +# MIT License (see LICENSE or https://opensource.org/licenses/MIT) |
| 9 | +''' |
| 10 | + |
| 11 | +import sys, os.path |
| 12 | +import requests as req |
| 13 | +import urllib.parse as up |
| 14 | +import random, string, json, re |
| 15 | +import logging |
| 16 | + |
| 17 | + |
| 18 | + |
| 19 | +class _Controller(object): |
| 20 | + """ Handler REST-API QRS""" |
| 21 | + |
| 22 | + _referer='Mozilla/5.0 (Windows NT 6.3; Win64; x64) qsAPI APIREST (QSense)' |
| 23 | + |
| 24 | + try: |
| 25 | + from requests_ntlm import HttpNtlmAuth as _ntlm |
| 26 | + except ImportError: |
| 27 | + _ntlm=None |
| 28 | + |
| 29 | + def __init__(self, schema, proxy, port, vproxy, certificate, verify, user, verbosity, logName): |
| 30 | + ''' |
| 31 | + @Function setup: Setup the connection and initialize handlers |
| 32 | + @param schema: http/https |
| 33 | + @param proxy: hostname to connect |
| 34 | + @param port: port number |
| 35 | + @param vproxy: virtual proxy conf. {preffix:'proxy', path: '^/qrs/', template:'/{}/qrs/'}) |
| 36 | + @param certificate: path to .pem client certificate |
| 37 | + @param verify: false to trust in self-signed certificates |
| 38 | + @param user: dict with keys {userDirectory:, userID:, password:} or tuple |
| 39 | + @param verbosity: debug level |
| 40 | + @param logger: logger instance name |
| 41 | + ''' |
| 42 | + self.proxy = proxy |
| 43 | + self.port = str(port) |
| 44 | + self.proxy = proxy; |
| 45 | + self.vproxy = None; |
| 46 | + self.baseurl = None |
| 47 | + self.request = None |
| 48 | + self.response = None |
| 49 | + self.session = None |
| 50 | + |
| 51 | + if vproxy: |
| 52 | + self.setVProxy(**vproxy) |
| 53 | + |
| 54 | + self.setUser(**user) if isinstance(user, dict) else self.setUser(*user) |
| 55 | + |
| 56 | + self.chunk_size = 512 #Kb |
| 57 | + |
| 58 | + self.log=logging.getLogger(logName) |
| 59 | + if not self.log.hasHandlers(): |
| 60 | + self.log.addHandler(logging.StreamHandler(sys.stdout)) |
| 61 | + self.log.setLevel(verbosity) |
| 62 | + |
| 63 | + self.baseurl= '{schema}://{host}:{port}'.format(schema=schema, host=proxy, port=str(port)) |
| 64 | + |
| 65 | + if isinstance(certificate, str): |
| 66 | + (base,ext)=os.path.splitext(certificate) |
| 67 | + self.cafile=(base+ext, base+'_key'+ext) |
| 68 | + self.log.debug('CERTKEY: %s%s', base, ext) |
| 69 | + elif isinstance(certificate, tuple): |
| 70 | + self.cafile=certificate |
| 71 | + self.log.debug('CERT: %s',certificate) |
| 72 | + else: |
| 73 | + self.cafile=False |
| 74 | + |
| 75 | + self._verify=bool(verify) |
| 76 | + |
| 77 | + if not self._verify: |
| 78 | + req.packages.urllib3.disable_warnings() |
| 79 | + |
| 80 | + self.session=req.Session() |
| 81 | + |
| 82 | + if self._ntlm and not self.cafile: |
| 83 | + self.log.debug('NTLM authentication enabled') |
| 84 | + self.session.auth = self._ntlm('{domain}\\{user}'.format(domain=self.UserDirectory, user=self.UserId), self.Password) |
| 85 | + |
| 86 | + |
| 87 | + def setVProxy(self, preffix, path, template): |
| 88 | + self.vproxy={} |
| 89 | + self.vproxy['preffix'] =preffix # proxy |
| 90 | + self.vproxy['path'] =re.compile(path) # ^/qrs/ |
| 91 | + self.vproxy['template']=template # /{}/qrs/ |
| 92 | + self.vproxy['pxpath'] =template.format(preffix) |
| 93 | + |
| 94 | + |
| 95 | + def setUser(self, userDirectory, userID, password=None): |
| 96 | + self.UserDirectory=userDirectory |
| 97 | + self.UserId = userID |
| 98 | + self.Password=password |
| 99 | + |
| 100 | + |
| 101 | + @staticmethod |
| 102 | + def normalize(schema, proxy, port, certificate): |
| 103 | + |
| 104 | + if '://' in proxy: |
| 105 | + schema, proxy = proxy.split('://') |
| 106 | + if not certificate and isinstance(port, int): |
| 107 | + port=443 |
| 108 | + if ':' in proxy: |
| 109 | + proxy, port = proxy.split(':') |
| 110 | + |
| 111 | + return(schema, proxy, port) |
| 112 | + |
| 113 | + |
| 114 | + def _params_prepare(self, param, xhd={}): |
| 115 | + |
| 116 | + par=dict({'Xrfkey': ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(16))}) |
| 117 | + if isinstance(param, dict): |
| 118 | + for p,v in param.items(): |
| 119 | + if v is not None: |
| 120 | + if isinstance(v, bool): |
| 121 | + par[p]=str(v).lower() |
| 122 | + else: |
| 123 | + par[p]=str(v) |
| 124 | + self.log.debug(" >> %s=>%s",p , par[p]) |
| 125 | + else: |
| 126 | + self.log.debug(" >> %s=>(default)", p) |
| 127 | + |
| 128 | + hd= { 'User-agent': self._referer, |
| 129 | + 'Pragma': 'no-cache', |
| 130 | + 'X-Qlik-User': 'UserDirectory={directory}; UserId={user}'.format(directory=self.UserDirectory, user=self.UserId), |
| 131 | + 'x-Qlik-Xrfkey': par.get('Xrfkey'), |
| 132 | + 'Accept': 'application/json', |
| 133 | + 'Content-Type': 'application/json'} |
| 134 | + |
| 135 | + if self.vproxy: |
| 136 | + hd['X-Qlik-Virtual-Proxy-Prefix']=self.vproxy['preffix'] |
| 137 | + |
| 138 | + hd.update(xhd) |
| 139 | + return(par, hd) |
| 140 | + |
| 141 | + |
| 142 | + |
| 143 | + def _params_update(self, url, par): |
| 144 | + scheme, netloc, path, query, fragment=up.urlsplit(url) |
| 145 | + if self.vproxy: |
| 146 | + path= self.vproxy['path'].sub(self.vproxy['pxpath'], path) |
| 147 | + p=up.parse_qs(query) |
| 148 | + p.update(par) |
| 149 | + query=up.urlencode(p,doseq=True,quote_via=up.quote) |
| 150 | + return up.urlunsplit((scheme, netloc, path, query, fragment)) |
| 151 | + |
| 152 | + |
| 153 | + |
| 154 | + |
| 155 | + def call(self, method, apipath, param=None, data=None, files=None): |
| 156 | + """ initialize control structure """ |
| 157 | + |
| 158 | + if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): |
| 159 | + raise ValueError('invalid method <{0}>'.format(method)) |
| 160 | + |
| 161 | + self.log.info('API %s <%s>', method[:3], apipath) |
| 162 | + |
| 163 | + (par,hd)=self._params_prepare(param, {} if files is None else {'Content-Type': 'application/vnd.qlik.sense.app'}) |
| 164 | + |
| 165 | + # Build the request |
| 166 | + self.response= None |
| 167 | + |
| 168 | + url=self._params_update(up.urljoin(self.baseurl,apipath), par) |
| 169 | + self.request=req.Request(method, url, headers=hd, data=data, files=files, auth=self.session.auth) |
| 170 | + pr=self.session.prepare_request(self.request) |
| 171 | + |
| 172 | + self.log.debug('SEND: %s', self.request.url) |
| 173 | + |
| 174 | + # Execute the HTTP request |
| 175 | + self.response = self.session.send(pr, cert=self.cafile, verify=self._verify, allow_redirects=False) |
| 176 | + rc=0 |
| 177 | + while self.response.is_redirect: |
| 178 | + rc+=1 |
| 179 | + if rc > self.session.max_redirects: |
| 180 | + raise req.HTTPError('Too many redirections') |
| 181 | + self.session.rebuild_auth(self.response.next, self.response) |
| 182 | + self.response.next.prepare_headers(hd) |
| 183 | + self.response.next.prepare_cookies(self.response.cookies) |
| 184 | + self.response.next.url=self._params_update(self.response.next.url, par) |
| 185 | + self.log.debug('REDIR: %s', self.response.next.url) |
| 186 | + self.response = self.session.send(self.response.next, verify=self._verify, allow_redirects=False) |
| 187 | + |
| 188 | + self.log.debug('RECV: %s',self.response.text) |
| 189 | + |
| 190 | + return(self.response) |
| 191 | + |
| 192 | + |
| 193 | + |
| 194 | + def download(self, apipath, filename, param=None): |
| 195 | + """ initialize control structure """ |
| 196 | + |
| 197 | + self.log.info('API DOWN <%s>', apipath) |
| 198 | + |
| 199 | + (par,hd)=self._params_prepare(param) |
| 200 | + |
| 201 | + # Build the request |
| 202 | + self.response= None |
| 203 | + |
| 204 | + url=self._params_update(up.urljoin(self.baseurl,apipath), par) |
| 205 | + |
| 206 | + self.log.debug('__SEND: %s',url) |
| 207 | + |
| 208 | + # Execute the HTTP request |
| 209 | + self.request = self.session.get(url, headers=hd, cert=self.cafile, verify=self._verify, stream=True, auth=self.session.auth) |
| 210 | + |
| 211 | + with open(filename, 'wb') as f: |
| 212 | + self.log.info('__Downloading (in %sKb blocks): ', str(self.chunk_size)) |
| 213 | + |
| 214 | + #download in 512Kb blocks |
| 215 | + for chunk in self.request.iter_content(chunk_size=self.chunk_size << 10): |
| 216 | + if chunk: # filter out keep-alive new chunks |
| 217 | + f.write(chunk) |
| 218 | + |
| 219 | + self.log.info('__Saved: %s', os.path.abspath(filename)) |
| 220 | + |
| 221 | + return(self.request) |
| 222 | + |
| 223 | + |
| 224 | + |
| 225 | + def upload(self, apipath, filename, param=None): |
| 226 | + """ initialize control structure """ |
| 227 | + |
| 228 | + class upload_in_chunks(object): |
| 229 | + def __init__(self, filename, chunksize=512): |
| 230 | + self.filename = filename |
| 231 | + self.chunksize = chunksize << 10 |
| 232 | + self.totalsize = os.path.getsize(filename) |
| 233 | + self.readsofar = 0 |
| 234 | + |
| 235 | + def __iter__(self): |
| 236 | + with open(self.filename, 'rb') as file: |
| 237 | + while True: |
| 238 | + data = file.read(self.chunksize) |
| 239 | + if not data: |
| 240 | + break |
| 241 | + self.readsofar += len(data) |
| 242 | + yield data |
| 243 | + |
| 244 | + def __len__(self): |
| 245 | + return self.totalsize |
| 246 | + |
| 247 | + self.log.info('API UPLO <%s>', apipath) |
| 248 | + |
| 249 | + (par,hd)=self._params_prepare(param, {'Content-Type': 'application/vnd.qlik.sense.app'}) |
| 250 | + |
| 251 | + # Build the request |
| 252 | + self.response= None |
| 253 | + url=self._params_update(up.urljoin(self.baseurl,apipath), par) |
| 254 | + self.log.debug('__SEND: %s', url) |
| 255 | + |
| 256 | + # Execute the HTTP request |
| 257 | + self.log.info('__Uploading {:,} bytes'.format(os.path.getsize(filename))) |
| 258 | + self.request = self.session.post(url, headers=hd, cert=self.cafile, verify=self._verify, \ |
| 259 | + data=upload_in_chunks(filename, self.chunk_size), auth=self.session.auth) |
| 260 | + |
| 261 | + self.log.info('__Done.') |
| 262 | + |
| 263 | + return(self.request) |
| 264 | + |
| 265 | + |
| 266 | + |
| 267 | + |
| 268 | + def get(self, apipath, param=None): |
| 269 | + ''' |
| 270 | + @Function get: generic purpose call |
| 271 | + @param apipath: uri REST path |
| 272 | + @param param : whatever other param needed in form a dict |
| 273 | + (example: {'filter': "name eq 'myApp'} ) |
| 274 | + ''' |
| 275 | + return self.call('GET', apipath, param) |
| 276 | + |
| 277 | + |
| 278 | + |
| 279 | + def post(self, apipath, param=None, data=None, files=None): |
| 280 | + ''' |
| 281 | + @Function post: generic purpose call |
| 282 | + @param apipath: uri REST path |
| 283 | + @param param : whatever other param needed in form a dict |
| 284 | + (example: {'filter': "name eq 'myApp'} ) |
| 285 | + @param data : stream data input (native dict/list structures are json formated) |
| 286 | + @param files : metafile input |
| 287 | + ''' |
| 288 | + if isinstance(data,dict) or isinstance(data,list): |
| 289 | + data=json.dumps(data) |
| 290 | + return self.call('POST', apipath, param, data, files) |
| 291 | + |
| 292 | + |
| 293 | + |
| 294 | + def put(self, apipath, param=None, data=None): |
| 295 | + ''' |
| 296 | + @Function put: generic purpose call |
| 297 | + @param apipath: uri REST path |
| 298 | + @param param : whatever other param needed in form a dict |
| 299 | + (example: {'filter': "name eq 'myApp'} ) |
| 300 | + @param data : stream data input (native dict/list structures are json formated) |
| 301 | + ''' |
| 302 | + if isinstance(data,dict) or isinstance(data,list): |
| 303 | + data=json.dumps(data) |
| 304 | + return self.call('PUT', apipath, param, data) |
| 305 | + |
| 306 | + |
| 307 | + |
| 308 | + def delete(self, apipath, param=None): |
| 309 | + ''' |
| 310 | + @Function delete: generic purpose call |
| 311 | + @param apipath: uri REST path |
| 312 | + @param param : whatever other param needed in form a dict |
| 313 | + (example: {'filter': "name eq 'myApp'} ) |
| 314 | + ''' |
| 315 | + return self.call('DELETE', apipath, param) |
| 316 | + |
0 commit comments