Issues (158)

doorpi/status/webserver_lib/request_handler.py (13 issues)

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
The import cgi seems to be unused.
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
0 ignored issues
show
The name unquote_plus does not seem to exist in module urllib.
Loading history...
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
Bug Best Practice introduced by Thomas
This seems to re-define the built-in format.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
The argument format seems to be unused.
Loading history...
45
    def log_message(self, format, *args): logger.debug("[%s] %s", self.client_address[0], args)
0 ignored issues
show
Bug Best Practice introduced by Thomas
This seems to re-define the built-in format.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
The argument format seems to be unused.
Loading history...
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 Thomas
Undefined variable 'control_config_get_value'
Loading history...
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 Thomas
Undefined variable 'control_config_set_value'
Loading history...
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 Thomas
Undefined variable 'control_config_delete_key'
Loading history...
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 Thomas
Undefined variable 'control_config_save'
Loading history...
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 Thomas
Undefined variable 'control_config_get_configfile'
Loading history...
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
The variable mime seems to be unused.
Loading history...
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
Bug Best Practice introduced by Thomas
This seems to re-define the built-in dir.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
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