| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame^] | 1 | # -*- coding: utf-8 -*- |
| 2 | |
| 3 | # |
| 4 | # Util functions previously in `httpserver` |
| 5 | # |
| 6 | |
| 7 | __author__ = "Alfonso Tierno, Gerardo Garcia" |
| 8 | |
| 9 | import json |
| 10 | import logging |
| 11 | |
| 12 | import bottle |
| 13 | import yaml |
| 14 | from jsonschema import exceptions as js_e |
| 15 | from jsonschema import validate as js_v |
| 16 | |
| 17 | from . import errors as httperrors |
| 18 | |
| 19 | logger = logging.getLogger('openmano.http') |
| 20 | |
| 21 | |
| 22 | def remove_clear_passwd(data): |
| 23 | """ |
| 24 | Removes clear passwords from the data received |
| 25 | :param data: data with clear password |
| 26 | :return: data without the password information |
| 27 | """ |
| 28 | |
| 29 | passw = ['password: ', 'passwd: '] |
| 30 | |
| 31 | for pattern in passw: |
| 32 | init = data.find(pattern) |
| 33 | while init != -1: |
| 34 | end = data.find('\n', init) |
| 35 | data = data[:init] + '{}******'.format(pattern) + data[end:] |
| 36 | init += 1 |
| 37 | init = data.find(pattern, init) |
| 38 | return data |
| 39 | |
| 40 | |
| 41 | def change_keys_http2db(data, http_db, reverse=False): |
| 42 | '''Change keys of dictionary data acording to the key_dict values |
| 43 | This allow change from http interface names to database names. |
| 44 | When reverse is True, the change is otherwise |
| 45 | Attributes: |
| 46 | data: can be a dictionary or a list |
| 47 | http_db: is a dictionary with hhtp names as keys and database names as value |
| 48 | reverse: by default change is done from http api to database. |
| 49 | If True change is done otherwise. |
| 50 | Return: None, but data is modified''' |
| 51 | if type(data) is tuple or type(data) is list: |
| 52 | for d in data: |
| 53 | change_keys_http2db(d, http_db, reverse) |
| 54 | elif type(data) is dict or type(data) is bottle.FormsDict: |
| 55 | if reverse: |
| 56 | for k,v in http_db.items(): |
| 57 | if v in data: data[k]=data.pop(v) |
| 58 | else: |
| 59 | for k,v in http_db.items(): |
| 60 | if k in data: data[v]=data.pop(k) |
| 61 | |
| 62 | |
| 63 | def format_out(data): |
| 64 | '''Return string of dictionary data according to requested json, yaml, xml. |
| 65 | By default json |
| 66 | ''' |
| 67 | logger.debug("OUT: " + yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False, encoding='utf-8', allow_unicode=True) ) |
| 68 | accept = bottle.request.headers.get('Accept') |
| 69 | if accept and 'application/yaml' in accept: |
| 70 | bottle.response.content_type='application/yaml' |
| 71 | return yaml.safe_dump( |
| 72 | data, explicit_start=True, indent=4, default_flow_style=False, |
| 73 | tags=False, encoding='utf-8', allow_unicode=True) #, canonical=True, default_style='"' |
| 74 | else: #by default json |
| 75 | bottle.response.content_type='application/json' |
| 76 | #return data #json no style |
| 77 | return json.dumps(data, indent=4) + "\n" |
| 78 | |
| 79 | |
| 80 | def format_in(default_schema, version_fields=None, version_dict_schema=None, confidential_data=False): |
| 81 | """ |
| 82 | Parse the content of HTTP request against a json_schema |
| 83 | |
| 84 | :param default_schema: The schema to be parsed by default |
| 85 | if no version field is found in the client data. |
| 86 | In None no validation is done |
| 87 | :param version_fields: If provided it contains a tuple or list with the |
| 88 | fields to iterate across the client data to obtain the version |
| 89 | :param version_dict_schema: It contains a dictionary with the version as key, |
| 90 | and json schema to apply as value. |
| 91 | It can contain a None as key, and this is apply |
| 92 | if the client data version does not match any key |
| 93 | :return: user_data, used_schema: if the data is successfully decoded and |
| 94 | matches the schema. |
| 95 | |
| 96 | Launch a bottle abort if fails |
| 97 | """ |
| 98 | #print "HEADERS :" + str(bottle.request.headers.items()) |
| 99 | try: |
| 100 | error_text = "Invalid header format " |
| 101 | format_type = bottle.request.headers.get('Content-Type', 'application/json') |
| 102 | if 'application/json' in format_type: |
| 103 | error_text = "Invalid json format " |
| 104 | #Use the json decoder instead of bottle decoder because it informs about the location of error formats with a ValueError exception |
| 105 | client_data = json.load(bottle.request.body) |
| 106 | #client_data = bottle.request.json() |
| 107 | elif 'application/yaml' in format_type: |
| 108 | error_text = "Invalid yaml format " |
| 109 | client_data = yaml.load(bottle.request.body) |
| 110 | elif 'application/xml' in format_type: |
| 111 | bottle.abort(501, "Content-Type: application/xml not supported yet.") |
| 112 | else: |
| 113 | logger.warning('Content-Type ' + str(format_type) + ' not supported.') |
| 114 | bottle.abort(httperrors.Not_Acceptable, 'Content-Type ' + str(format_type) + ' not supported.') |
| 115 | return |
| 116 | # if client_data == None: |
| 117 | # bottle.abort(httperrors.Bad_Request, "Content error, empty") |
| 118 | # return |
| 119 | if confidential_data: |
| 120 | logger.debug('IN: %s', remove_clear_passwd (yaml.safe_dump(client_data, explicit_start=True, indent=4, default_flow_style=False, |
| 121 | tags=False, encoding='utf-8', allow_unicode=True))) |
| 122 | else: |
| 123 | logger.debug('IN: %s', yaml.safe_dump(client_data, explicit_start=True, indent=4, default_flow_style=False, |
| 124 | tags=False, encoding='utf-8', allow_unicode=True) ) |
| 125 | # look for the client provider version |
| 126 | error_text = "Invalid content " |
| 127 | if not default_schema and not version_fields: |
| 128 | return client_data, None |
| 129 | client_version = None |
| 130 | used_schema = None |
| 131 | if version_fields != None: |
| 132 | client_version = client_data |
| 133 | for field in version_fields: |
| 134 | if field in client_version: |
| 135 | client_version = client_version[field] |
| 136 | else: |
| 137 | client_version=None |
| 138 | break |
| 139 | if client_version == None: |
| 140 | used_schema = default_schema |
| 141 | elif version_dict_schema != None: |
| 142 | if client_version in version_dict_schema: |
| 143 | used_schema = version_dict_schema[client_version] |
| 144 | elif None in version_dict_schema: |
| 145 | used_schema = version_dict_schema[None] |
| 146 | if used_schema==None: |
| 147 | bottle.abort(httperrors.Bad_Request, "Invalid schema version or missing version field") |
| 148 | |
| 149 | js_v(client_data, used_schema) |
| 150 | return client_data, used_schema |
| 151 | except (TypeError, ValueError, yaml.YAMLError) as exc: |
| 152 | error_text += str(exc) |
| 153 | logger.error(error_text, exc_info=True) |
| 154 | bottle.abort(httperrors.Bad_Request, error_text) |
| 155 | except js_e.ValidationError as exc: |
| 156 | logger.error( |
| 157 | "validate_in error, jsonschema exception", exc_info=True) |
| 158 | error_pos = "" |
| 159 | if len(exc.path)>0: error_pos=" at " + ":".join(map(json.dumps, exc.path)) |
| 160 | bottle.abort(httperrors.Bad_Request, error_text + exc.message + error_pos) |
| 161 | #except: |
| 162 | # bottle.abort(httperrors.Bad_Request, "Content error: Failed to parse Content-Type", error_pos) |
| 163 | # raise |
| 164 | |
| 165 | def filter_query_string(qs, http2db, allowed): |
| 166 | '''Process query string (qs) checking that contains only valid tokens for avoiding SQL injection |
| 167 | Attributes: |
| 168 | 'qs': bottle.FormsDict variable to be processed. None or empty is considered valid |
| 169 | 'http2db': dictionary with change from http API naming (dictionary key) to database naming(dictionary value) |
| 170 | 'allowed': list of allowed string tokens (API http naming). All the keys of 'qs' must be one of 'allowed' |
| 171 | Return: A tuple with the (select,where,limit) to be use in a database query. All of then transformed to the database naming |
| 172 | select: list of items to retrieve, filtered by query string 'field=token'. If no 'field' is present, allowed list is returned |
| 173 | where: dictionary with key, value, taken from the query string token=value. Empty if nothing is provided |
| 174 | limit: limit dictated by user with the query string 'limit'. 100 by default |
| 175 | abort if not permited, using bottel.abort |
| 176 | ''' |
| 177 | where={} |
| 178 | limit=100 |
| 179 | select=[] |
| 180 | #if type(qs) is not bottle.FormsDict: |
| 181 | # bottle.abort(httperrors.Internal_Server_Error, '!!!!!!!!!!!!!!invalid query string not a dictionary') |
| 182 | # #bottle.abort(httperrors.Internal_Server_Error, "call programmer") |
| 183 | for k in qs: |
| 184 | if k=='field': |
| 185 | select += qs.getall(k) |
| 186 | for v in select: |
| 187 | if v not in allowed: |
| 188 | bottle.abort(httperrors.Bad_Request, "Invalid query string at 'field="+v+"'") |
| 189 | elif k=='limit': |
| 190 | try: |
| 191 | limit=int(qs[k]) |
| 192 | except: |
| 193 | bottle.abort(httperrors.Bad_Request, "Invalid query string at 'limit="+qs[k]+"'") |
| 194 | else: |
| 195 | if k not in allowed: |
| 196 | bottle.abort(httperrors.Bad_Request, "Invalid query string at '"+k+"="+qs[k]+"'") |
| 197 | if qs[k]!="null": where[k]=qs[k] |
| 198 | else: where[k]=None |
| 199 | if len(select)==0: select += allowed |
| 200 | #change from http api to database naming |
| 201 | for i in range(0,len(select)): |
| 202 | k=select[i] |
| 203 | if http2db and k in http2db: |
| 204 | select[i] = http2db[k] |
| 205 | if http2db: |
| 206 | change_keys_http2db(where, http2db) |
| 207 | #print "filter_query_string", select,where,limit |
| 208 | |
| 209 | return select,where,limit |