1 | #!/usr/bin/env python |
||
2 | # -*- coding: utf-8 -*- |
||
3 | |||
4 | import logging |
||
5 | logger = logging.getLogger(__name__) |
||
6 | logger.debug("%s loaded", __name__) |
||
7 | |||
8 | import os |
||
9 | from mimetypes import guess_type |
||
10 | from BaseHTTPServer import BaseHTTPRequestHandler |
||
11 | import cgi # for parsing POST |
||
0 ignored issues
–
show
Unused Code
introduced
by
Loading history...
|
|||
12 | from urlparse import urlparse, parse_qs # parsing parameters and url |
||
13 | import re # regex for area |
||
14 | import json # for virtual resources |
||
15 | from urllib2 import urlopen as load_online_fallback |
||
16 | from urllib import unquote_plus |
||
17 | |||
18 | from doorpi.action.base import SingleAction |
||
19 | import doorpi |
||
20 | from request_handler_static_functions import * |
||
21 | |||
22 | VIRTUELL_RESOURCES = [ |
||
23 | '/mirror', |
||
24 | '/status', |
||
25 | '/control/trigger_event', |
||
26 | '/control/config_value_get', |
||
27 | '/control/config_value_set', |
||
28 | '/control/config_value_delete', |
||
29 | '/control/config_save', |
||
30 | '/control/config_get_configfile', |
||
31 | '/help/modules.overview.html' |
||
32 | ] |
||
33 | |||
34 | DOORPIWEB_SECTION = 'DoorPiWeb' |
||
35 | |||
36 | class WebServerLoginRequired(Exception): pass |
||
37 | class WebServerRequestHandlerShutdownAction(SingleAction): pass |
||
38 | |||
39 | class DoorPiWebRequestHandler(BaseHTTPRequestHandler): |
||
40 | |||
41 | @property |
||
42 | def conf(self): return self.server.config |
||
43 | |||
44 | def log_error(self, format, *args): logger.error("[%s] %s", self.client_address[0], args) |
||
0 ignored issues
–
show
|
|||
45 | def log_message(self, format, *args): logger.debug("[%s] %s", self.client_address[0], args) |
||
0 ignored issues
–
show
|
|||
46 | |||
47 | @staticmethod |
||
48 | def prepare(): |
||
49 | doorpi.DoorPi().event_handler.register_event('OnWebServerRequest', __name__) |
||
50 | doorpi.DoorPi().event_handler.register_event('OnWebServerRequestGet', __name__) |
||
51 | doorpi.DoorPi().event_handler.register_event('OnWebServerRequestPost', __name__) |
||
52 | doorpi.DoorPi().event_handler.register_event('OnWebServerVirtualResource', __name__) |
||
53 | doorpi.DoorPi().event_handler.register_event('OnWebServerRealResource', __name__) |
||
54 | |||
55 | # for do_control |
||
56 | doorpi.DoorPi().event_handler.register_event('OnFireEvent', __name__) |
||
57 | doorpi.DoorPi().event_handler.register_event('OnConfigKeySet', __name__) |
||
58 | doorpi.DoorPi().event_handler.register_event('OnConfigKeyDelete', __name__) |
||
59 | |||
60 | @staticmethod |
||
61 | def destroy(): |
||
62 | doorpi.DoorPi().event_handler.unregister_source( __name__, True) |
||
63 | |||
64 | def do_GET(self): |
||
65 | #doorpi.DoorPi().event_handler('OnWebServerRequest', __name__) |
||
66 | if not self.server.keep_running: return |
||
67 | |||
68 | parsed_path = urlparse(self.path) |
||
69 | #doorpi.DoorPi().event_handler('OnWebServerRequest', __name__, {'header': self.headers.items(), 'path': parsed_path}) |
||
70 | #doorpi.DoorPi().event_handler('OnWebServerRequestGet', __name__, {'header': self.headers.items(), 'path': parsed_path}) |
||
71 | |||
72 | if parsed_path.path == "/": |
||
73 | return self.return_redirection('dashboard/pages/index.html') |
||
74 | |||
75 | if self.authentication_required(): return self.login_form() |
||
76 | |||
77 | #doorpi.DoorPi().event_handler('OnWebServerRequestGet', __name__) |
||
78 | |||
79 | if parsed_path.path in VIRTUELL_RESOURCES: |
||
80 | return self.create_virtual_resource(parsed_path, parse_qs(urlparse(self.path).query)) |
||
81 | else: return self.real_resource(parsed_path.path) |
||
82 | |||
83 | def do_control(self, control_order, para): |
||
84 | result_object = dict( |
||
85 | success = False, |
||
86 | message = 'unknown error' |
||
87 | ) |
||
88 | logger.debug(json.dumps(para, sort_keys = True, indent = 4)) |
||
89 | |||
90 | try: |
||
91 | for parameter_name in para.keys(): |
||
92 | try: para[parameter_name] = unquote_plus(para[parameter_name][0]) |
||
93 | except KeyError: para[parameter_name] = '' |
||
94 | except IndexError: para[parameter_name] = '' |
||
95 | |||
96 | if control_order == "trigger_event": |
||
97 | result_object['message'] = doorpi.DoorPi().event_handler.fire_event_synchron(**para) |
||
98 | if result_object['message'] is True: |
||
99 | result_object['success'] = True |
||
100 | result_object['message'] = "fire Event was success" |
||
101 | else: |
||
102 | result_object['success'] = False |
||
103 | elif control_order == "config_value_get": |
||
104 | # section, key, default, store |
||
105 | result_object['success'] = True |
||
106 | result_object['message'] = control_config_get_value(**para) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
107 | elif control_order == "config_value_set": |
||
108 | # section, key, value, password |
||
109 | result_object['success'] = control_config_set_value(**para) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
110 | result_object['message'] = "config_value_set %s" % ( |
||
111 | 'success' if result_object['success'] else 'failed' |
||
112 | ) |
||
113 | elif control_order == "config_value_delete": |
||
114 | # section and key |
||
115 | result_object['success'] = control_config_delete_key(**para) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
116 | result_object['message'] = "config_value_delete %s" % ( |
||
117 | 'success' if result_object['success'] else 'failed' |
||
118 | ) |
||
119 | elif control_order == "config_save": |
||
120 | # configfile |
||
121 | result_object['success'] = control_config_save(**para) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
122 | result_object['message'] = "config_save %s" % ( |
||
123 | 'success' if result_object['success'] else 'failed' |
||
124 | ) |
||
125 | elif control_order == "config_get_configfile": |
||
126 | result_object['message'] = control_config_get_configfile() |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
127 | result_object['success'] = True if result_object['message'] != "" else False |
||
128 | |||
129 | except Exception as exp: |
||
130 | result_object['message'] = str(exp) |
||
131 | |||
132 | return result_object |
||
133 | |||
134 | def clear_parameters(self, raw_parameters): |
||
135 | if 'module' not in raw_parameters.keys(): raw_parameters['module'] = [] |
||
136 | if 'name' not in raw_parameters.keys(): raw_parameters['name'] = [] |
||
137 | if 'value' not in raw_parameters.keys(): raw_parameters['value'] = [] |
||
138 | return raw_parameters |
||
139 | |||
140 | def create_virtual_resource(self, path, raw_parameters): |
||
141 | return_object = {} |
||
142 | try: |
||
143 | if path.path == '/mirror': |
||
144 | return_object = self.create_mirror() |
||
145 | raw_parameters['output'] = "string" |
||
146 | elif path.path == '/status': |
||
147 | raw_parameters = self.clear_parameters(raw_parameters) |
||
148 | return_object = doorpi.DoorPi().get_status( |
||
149 | modules = raw_parameters['module'], |
||
150 | name = raw_parameters['name'], |
||
151 | value = raw_parameters['value'] |
||
152 | ).dictionary |
||
153 | elif path.path.startswith('/control/'): |
||
154 | return_object = self.do_control(path.path.split('/')[-1], raw_parameters) |
||
155 | elif path.path == '/help/modules.overview.html': |
||
156 | raw_parameters = self.clear_parameters(raw_parameters) |
||
157 | return_object, mime = self.get_file_content('/dashboard/parts/modules.overview.html') |
||
0 ignored issues
–
show
|
|||
158 | return_object = self.parse_content( |
||
159 | return_object, |
||
160 | MODULE_AREA_NAME = raw_parameters['module'][0] or '', |
||
161 | MODULE_NAME = raw_parameters['name'][0] or '' |
||
162 | ) |
||
163 | raw_parameters['output'] = "html" |
||
164 | except Exception as exp: return_object = dict(error_message = str(exp)) |
||
165 | |||
166 | if 'output' not in raw_parameters.keys(): raw_parameters['output'] = '' |
||
167 | return self.return_virtual_resource(return_object, raw_parameters['output']) |
||
168 | |||
169 | def return_virtual_resource(self, prepared_object, return_type = 'json'): |
||
170 | if isinstance(return_type, list) and len(return_type) > 0: return_type = return_type[0] |
||
171 | |||
172 | if return_type in ["json", "default"]: |
||
173 | return self.return_message(json.dumps(prepared_object), "application/json; charset=utf-8") |
||
174 | if return_type in ["json_parsed", "json.parsed"]: |
||
175 | return self.return_message(self.parse_content(json.dumps(prepared_object)), "application/json; charset=utf-8") |
||
176 | elif return_type in ["json_beautified", "json.beautified", "beautified.json"]: |
||
177 | return self.return_message(json.dumps(prepared_object, sort_keys=True, indent=4), "application/json; charset=utf-8") |
||
178 | elif return_type in ["json_beautified_parsed", "json.beautified.parsed", "beautified.json.parsed", ""]: |
||
179 | return self.return_message(self.parse_content(json.dumps(prepared_object, sort_keys=True, indent=4)), "application/json; charset=utf-8") |
||
180 | elif return_type in ["string", "plain", "str"]: |
||
181 | return self.return_message(str(prepared_object)) |
||
182 | elif return_type in ["repr"]: |
||
183 | return self.return_message(repr(prepared_object)) |
||
184 | elif return_type is 'html': |
||
185 | return self.return_message(prepared_object, 'text/html; charset=utf-8') |
||
186 | else: |
||
187 | try: return self.return_message(repr(prepared_object)) |
||
188 | except: return self.return_message(str(prepared_object)) |
||
189 | pass |
||
190 | |||
191 | def real_resource(self, path): |
||
192 | #doorpi.DoorPi().event_handler('OnWebServerRealResource', __name__, {'path': path}) |
||
193 | if os.path.isdir(self.server.www + path): return self.list_directory(self.server.www + path) |
||
194 | try: |
||
195 | return self.return_file_content(path) |
||
196 | except IOError as exp: |
||
197 | return self.real_resource_fallback(path, exp) |
||
198 | except Exception as exp: |
||
199 | logger.exception(exp) |
||
200 | return self.send_error(500, str(exp)) |
||
201 | |||
202 | def real_resource_fallback(self, path, previous_exception): |
||
203 | try: |
||
204 | return self.return_fallback_content(self.server.online_fallback, path) |
||
205 | except IOError: |
||
206 | return self.send_error(404, str(previous_exception)) |
||
207 | except Exception as exp: |
||
208 | logger.exception(exp) |
||
209 | return self.send_error(500, str(exp)) |
||
210 | |||
211 | def list_directory(self, path): |
||
212 | dirs = [] |
||
213 | files = [] |
||
214 | for item in os.listdir(path): |
||
215 | if os.path.isfile(item): files.append(item) |
||
216 | else: dirs.append(item) |
||
217 | |||
218 | return_html = '''<!DOCTYPE html> |
||
219 | <html lang="en"><head></head><body><a href="..">..</a></br> |
||
220 | ''' |
||
221 | for dir in dirs: return_html += '<a href="./{dir}">{dir}</a></br>'.format(dir = dir) |
||
0 ignored issues
–
show
|
|||
222 | for file in files: return_html += '<a href="./{file}">{file}</a></br>'.format(file = file) |
||
223 | return_html += "</body></html>" |
||
224 | return self.return_message(return_html, 'text/html') |
||
225 | #return self.send_error(403) |
||
226 | |||
227 | def return_redirection(self, new_location): |
||
228 | message = ''' |
||
229 | <html> |
||
230 | <meta http-equiv="refresh" content="0;url={new_location}"> |
||
231 | <a href="{new_location}">{new_location}</a> |
||
232 | </html> |
||
233 | '''.format(new_location = new_location) |
||
234 | return self.return_message(message, 'text/html', http_code = 301) |
||
235 | |||
236 | @staticmethod |
||
237 | def get_mime_typ(url): |
||
238 | return guess_type(url)[0] or "" |
||
239 | |||
240 | @staticmethod |
||
241 | def is_file_parsable(filename): |
||
242 | mime_type = DoorPiWebRequestHandler.get_mime_typ(filename) |
||
243 | return mime_type in ['text/html'] |
||
244 | |||
245 | def read_from_file(self, url): |
||
246 | try: |
||
247 | read_mode = "r" if self.is_file_parsable(url) else "rb" |
||
248 | with open(url, read_mode) as file: |
||
249 | file_content = file.read() |
||
250 | if self.is_file_parsable(url): |
||
251 | return self.parse_content(file_content) |
||
252 | else: |
||
253 | return self.parse_content(file_content) |
||
254 | except Exception as exp: |
||
255 | raise exp |
||
256 | |||
257 | def read_from_fallback(self, url): |
||
258 | response = load_online_fallback(url, timeout = 1) |
||
259 | if self.is_file_parsable(url): |
||
260 | return self.parse_content(response.read(), True) |
||
261 | else: |
||
262 | return response.read() |
||
263 | |||
264 | def get_file_content(self, path): |
||
265 | content = mime = "" |
||
266 | try: |
||
267 | content = self.read_from_file(self.server.www + path) |
||
268 | mime = self.get_mime_typ(self.server.www + path) |
||
269 | except Exception as first_exp: |
||
270 | try: |
||
271 | logger.trace('use onlinefallback - local file %s not found', self.server.www + path) |
||
272 | content = self.read_from_fallback(self.server.online_fallback + path) |
||
273 | mime = self.get_mime_typ(self.server.online_fallback + path) |
||
274 | except Exception as exp: |
||
275 | return self.send_error(404, str(first_exp)+" - "+str(exp)) |
||
276 | |||
277 | return content, mime |
||
278 | |||
279 | def return_file_content(self, path): |
||
280 | content, mime = self.get_file_content(path) |
||
281 | return self.return_message( |
||
282 | content, mime |
||
283 | ) |
||
284 | |||
285 | def return_message(self, message = "", content_type = 'text/plain; charset=utf-8', http_code = 200,): |
||
286 | self.send_response(http_code) |
||
287 | #if login_form: |
||
288 | self.send_header('WWW-Authenticate', 'Basic realm=\"%s\"' % doorpi.DoorPi().name_and_version) |
||
289 | self.send_header("Server", doorpi.DoorPi().name_and_version) |
||
290 | self.send_header("Content-type", content_type) |
||
291 | self.send_header('Connection', 'close') |
||
292 | self.end_headers() |
||
293 | self.wfile.write(message) |
||
294 | |||
295 | def login_form(self): |
||
296 | try: |
||
297 | login_form_content = self.read_from_file(self.server.www + "/" + self.server.loginfile) |
||
298 | except IOError: |
||
299 | logger.info('Missing login file: '+ self.server.loginfile) |
||
300 | login_form_content = ''' |
||
301 | <head> |
||
302 | <title>Error response</title> |
||
303 | </head> |
||
304 | <body> |
||
305 | <h1>Error response</h1> |
||
306 | <p>Error code 401. |
||
307 | <p>Message: <a href="http://tools.ietf.org/html/rfc7235#section-3.1">RFC7235 - 401 Unauthorized</a> |
||
308 | <p>Error code explanation: 401 = Unauthorized. |
||
309 | </body> |
||
310 | ''' |
||
311 | except Exception as exp: |
||
312 | logger.exception(exp) |
||
313 | self.send_error(500, str(exp)) |
||
314 | return False |
||
315 | |||
316 | self.return_message( |
||
317 | message = self.parse_content(login_form_content), |
||
318 | content_type = 'text/html; charset=utf-8', |
||
319 | http_code = 401 |
||
320 | ) |
||
321 | return True |
||
322 | |||
323 | def authentication_required(self): |
||
324 | parsed_path = urlparse(self.path) |
||
325 | |||
326 | public_resources = self.conf.get_keys(self.server.area_public_name, log = False) |
||
327 | for public_resource in public_resources: |
||
328 | if re.match(public_resource, parsed_path.path): |
||
329 | logger.debug('public resource: %s',parsed_path.path) |
||
330 | return False |
||
331 | |||
332 | try: |
||
333 | username, password = self.headers['authorization'].replace('Basic ', '').decode('base64').split(':', 1) |
||
334 | except Exception as exp: |
||
335 | logger.debug('no header Authorization object (%s)', exp) |
||
336 | return True |
||
337 | |||
338 | user_session = self.server.sessions.get_session(username) |
||
339 | if not user_session: |
||
340 | user_session = self.server.sessions.build_security_object(username, password) |
||
341 | |||
342 | if not user_session: |
||
343 | logger.debug('need authentication (no session): %s', parsed_path.path) |
||
344 | return True |
||
345 | |||
346 | for write_permission in user_session['writepermissions']: |
||
347 | if re.match(write_permission, parsed_path.path): |
||
348 | logger.info('user %s has write permissions: %s', user_session['username'], parsed_path.path) |
||
349 | return False |
||
350 | |||
351 | for read_permission in user_session['readpermissions']: |
||
352 | if re.match(read_permission, parsed_path.path): |
||
353 | logger.info('user %s has read permissions: %s', user_session['username'], parsed_path.path) |
||
354 | return False |
||
355 | |||
356 | logger.warning('user %s has no permissions: %s', user_session['username'], parsed_path.path) |
||
357 | return True |
||
358 | |||
359 | def check_authentication(self): |
||
360 | if not self.authentication_required(): return True |
||
361 | raise WebServerLoginRequired() |
||
362 | |||
363 | def create_mirror(self): |
||
364 | parsed_path = urlparse(self.path) |
||
365 | message_parts = [ |
||
366 | 'CLIENT VALUES:', |
||
367 | 'client_address=%s (%s)' % (self.client_address, |
||
368 | self.address_string()), |
||
369 | 'raw_requestline=%s' % self.raw_requestline, |
||
370 | 'command=%s' % self.command, |
||
371 | 'path=%s' % self.path, |
||
372 | 'real path=%s' % parsed_path.path, |
||
373 | 'query=%s' % parsed_path.query, |
||
374 | 'request_version=%s' % self.request_version, |
||
375 | '', |
||
376 | 'SERVER VALUES:', |
||
377 | 'server_version=%s' % self.server_version, |
||
378 | 'sys_version=%s' % self.sys_version, |
||
379 | 'protocol_version=%s' % self.protocol_version, |
||
380 | '', |
||
381 | 'HEADERS RECEIVED:', |
||
382 | ] |
||
383 | for name, value in sorted(self.headers.items()): |
||
384 | message_parts.append('%s=%s' % (name, value.rstrip())) |
||
385 | message_parts.append('') |
||
386 | message = '\r\n'.join(message_parts) |
||
387 | return message |
||
388 | |||
389 | def parse_content(self, content, online_fallback = False, **mapping_table): |
||
390 | try: |
||
391 | matches = re.findall(r"{([^}\s]*)}", content) |
||
392 | if not matches: return content |
||
393 | #http://stackoverflow.com/questions/12897374/get-unique-values-from-a-list-in-python/12897491#12897491 |
||
394 | matches = list(set(matches)) |
||
395 | |||
396 | mapping_table['DOORPI'] = doorpi.DoorPi().name_and_version |
||
397 | mapping_table['SERVER'] = self.server.server_name |
||
398 | mapping_table['PORT'] = str(self.server.server_port) |
||
399 | mapping_table['MIN_EXTENSION'] = '' if logger.getEffectiveLevel() <= 5 else '.min' |
||
400 | |||
401 | #nutze den Hostnamen aus der URL. sonst ist ein erneuter Login nötig |
||
402 | if 'host' in self.headers.keys(): |
||
403 | mapping_table['BASE_URL'] = "http://%s"%self.headers['host'] |
||
404 | else: |
||
405 | mapping_table['BASE_URL'] = "http://%s:%s"%(self.server.server_name, self.server.server_port) |
||
406 | |||
407 | # Trennung DATA_URL (AJAX) und BASE_URL (Dateien) |
||
408 | mapping_table['DATA_URL'] = mapping_table['BASE_URL'] |
||
409 | |||
410 | if online_fallback and self.server.online_fallback: |
||
411 | mapping_table['BASE_URL'] = self.server.online_fallback |
||
412 | |||
413 | # Templates: |
||
414 | mapping_table['TEMPLATE:HTML_HEADER'] = 'html.header.html' |
||
415 | mapping_table['TEMPLATE:HTML_FOOTER'] = 'html.footer.html' |
||
416 | mapping_table['TEMPLATE:NAVIGATION'] = 'navigation.html' |
||
417 | |||
418 | for match in matches: |
||
419 | if match not in mapping_table.keys(): continue |
||
420 | if match.startswith('TEMPLATE:'): |
||
421 | try: replace_with = self.read_from_file(self.server.www + '/dashboard/parts/' + mapping_table[match]) |
||
422 | except IOError: |
||
423 | if self.server.online_fallback: |
||
424 | replace_with = self.read_from_fallback( |
||
425 | self.server.online_fallback + |
||
426 | '/dashboard/parts/' + |
||
427 | mapping_table[match] |
||
428 | ) |
||
429 | else: |
||
430 | replace_with = "" |
||
431 | except Exception: replace_with = "" |
||
432 | content = content.replace( |
||
433 | '{'+match+'}', |
||
434 | replace_with or "" |
||
435 | ) |
||
436 | else: |
||
437 | content = content.replace('{'+match+'}', mapping_table[match]) |
||
438 | |||
439 | except Exception as exp: |
||
440 | logger.exception(exp) |
||
441 | finally: |
||
442 | return content |
||
443 |