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