motom001/DoorPi

View on GitHub
doorpi/status/webserver_lib/request_handler.py

Summary

Maintainability
F
3 days
Test Coverage
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging
logger = logging.getLogger(__name__)
logger.debug("%s loaded", __name__)

import os
from mimetypes import guess_type
from BaseHTTPServer import BaseHTTPRequestHandler
import cgi # for parsing POST
from urlparse import urlparse, parse_qs # parsing parameters and url
import re # regex for area
import json # for virtual resources
from urllib2 import urlopen as load_online_fallback
from urllib import unquote_plus

from doorpi.action.base import SingleAction
import doorpi
from request_handler_static_functions import *

VIRTUELL_RESOURCES = [
    '/mirror',
    '/status',
    '/control/trigger_event',
    '/control/config_value_get',
    '/control/config_value_set',
    '/control/config_value_delete',
    '/control/config_save',
    '/control/config_get_configfile',
    '/help/modules.overview.html'
]

DOORPIWEB_SECTION = 'DoorPiWeb'

class WebServerLoginRequired(Exception): pass
class WebServerRequestHandlerShutdownAction(SingleAction): pass

class DoorPiWebRequestHandler(BaseHTTPRequestHandler):

    @property
    def conf(self): return self.server.config

    def log_error(self, format, *args): logger.error("[%s] %s", self.client_address[0], args)
    def log_message(self, format, *args): logger.debug("[%s] %s", self.client_address[0], args)

    @staticmethod
    def prepare():
        doorpi.DoorPi().event_handler.register_event('OnWebServerRequest', __name__)
        doorpi.DoorPi().event_handler.register_event('OnWebServerRequestGet', __name__)
        doorpi.DoorPi().event_handler.register_event('OnWebServerRequestPost', __name__)
        doorpi.DoorPi().event_handler.register_event('OnWebServerVirtualResource', __name__)
        doorpi.DoorPi().event_handler.register_event('OnWebServerRealResource', __name__)

        # for do_control
        doorpi.DoorPi().event_handler.register_event('OnFireEvent', __name__)
        doorpi.DoorPi().event_handler.register_event('OnConfigKeySet', __name__)
        doorpi.DoorPi().event_handler.register_event('OnConfigKeyDelete', __name__)

    @staticmethod
    def destroy():
        doorpi.DoorPi().event_handler.unregister_source( __name__, True)

    def do_GET(self):
        #doorpi.DoorPi().event_handler('OnWebServerRequest', __name__)
        if not self.server.keep_running: return

        parsed_path = urlparse(self.path)
        #doorpi.DoorPi().event_handler('OnWebServerRequest', __name__, {'header': self.headers.items(), 'path': parsed_path})
        #doorpi.DoorPi().event_handler('OnWebServerRequestGet', __name__, {'header': self.headers.items(), 'path': parsed_path})

        if parsed_path.path == "/":
            return self.return_redirection('dashboard/pages/index.html')

        if self.authentication_required(): return self.login_form()

        #doorpi.DoorPi().event_handler('OnWebServerRequestGet', __name__)

        if parsed_path.path in VIRTUELL_RESOURCES:
            return self.create_virtual_resource(parsed_path, parse_qs(urlparse(self.path).query))
        else: return self.real_resource(parsed_path.path)

    def do_control(self, control_order, para):
        result_object = dict(
            success = False,
            message = 'unknown error'
        )
        logger.debug(json.dumps(para, sort_keys = True, indent = 4))

        try:
            for parameter_name in para.keys():
                try:                    para[parameter_name] = unquote_plus(para[parameter_name][0])
                except KeyError:        para[parameter_name] = ''
                except IndexError:      para[parameter_name] = ''

            if control_order == "trigger_event":
                result_object['message'] = doorpi.DoorPi().event_handler.fire_event_synchron(**para)
                if result_object['message'] is True:
                    result_object['success'] = True
                    result_object['message'] = "fire Event was success"
                else:
                    result_object['success'] = False
            elif control_order == "config_value_get":
                # section, key, default, store
                result_object['success'] = True
                result_object['message'] = control_config_get_value(**para)
            elif control_order == "config_value_set":
                # section, key, value, password
                result_object['success'] = control_config_set_value(**para)
                result_object['message'] = "config_value_set %s" % (
                    'success' if result_object['success'] else 'failed'
                )
            elif control_order == "config_value_delete":
                # section and key
                result_object['success'] = control_config_delete_key(**para)
                result_object['message'] = "config_value_delete %s" % (
                    'success' if result_object['success'] else 'failed'
                )
            elif control_order == "config_save":
                # configfile
                result_object['success'] = control_config_save(**para)
                result_object['message'] = "config_save %s" % (
                    'success' if result_object['success'] else 'failed'
                )
            elif control_order == "config_get_configfile":
                result_object['message'] = control_config_get_configfile()
                result_object['success'] = True if result_object['message'] != "" else False

        except Exception as exp:
            result_object['message'] = str(exp)

        return result_object

    def clear_parameters(self, raw_parameters):
        if 'module' not in raw_parameters.keys(): raw_parameters['module'] = []
        if 'name' not in raw_parameters.keys(): raw_parameters['name'] = []
        if 'value' not in raw_parameters.keys(): raw_parameters['value'] = []
        return raw_parameters

    def create_virtual_resource(self, path, raw_parameters):
        return_object = {}
        try:
            if path.path == '/mirror':
                return_object = self.create_mirror()
                raw_parameters['output'] = "string"
            elif path.path == '/status':
                raw_parameters = self.clear_parameters(raw_parameters)
                return_object = doorpi.DoorPi().get_status(
                    modules = raw_parameters['module'],
                    name = raw_parameters['name'],
                    value = raw_parameters['value']
                ).dictionary
            elif path.path.startswith('/control/'):
                return_object = self.do_control(path.path.split('/')[-1], raw_parameters)
            elif path.path == '/help/modules.overview.html':
                raw_parameters = self.clear_parameters(raw_parameters)
                return_object, mime = self.get_file_content('/dashboard/parts/modules.overview.html')
                return_object = self.parse_content(
                    return_object,
                    MODULE_AREA_NAME = raw_parameters['module'][0] or '',
                    MODULE_NAME = raw_parameters['name'][0] or ''
                )
                raw_parameters['output'] = "html"
        except Exception as exp: return_object = dict(error_message = str(exp))

        if 'output' not in raw_parameters.keys(): raw_parameters['output'] = ''
        return self.return_virtual_resource(return_object, raw_parameters['output'])

    def return_virtual_resource(self, prepared_object, return_type = 'json'):
        if isinstance(return_type, list) and len(return_type) > 0: return_type = return_type[0]

        if return_type in ["json", "default"]:
            return  self.return_message(json.dumps(prepared_object), "application/json; charset=utf-8")
        if return_type in ["json_parsed", "json.parsed"]:
            return  self.return_message(self.parse_content(json.dumps(prepared_object)), "application/json; charset=utf-8")
        elif return_type in ["json_beautified", "json.beautified", "beautified.json"]:
            return  self.return_message(json.dumps(prepared_object, sort_keys=True, indent=4), "application/json; charset=utf-8")
        elif return_type in ["json_beautified_parsed", "json.beautified.parsed", "beautified.json.parsed", ""]:
            return  self.return_message(self.parse_content(json.dumps(prepared_object, sort_keys=True, indent=4)), "application/json; charset=utf-8")
        elif return_type in ["string", "plain", "str"]:
            return self.return_message(str(prepared_object))
        elif return_type in ["repr"]:
            return self.return_message(repr(prepared_object))
        elif return_type is 'html':
            return self.return_message(prepared_object, 'text/html; charset=utf-8')
        else:
            try:    return self.return_message(repr(prepared_object))
            except: return self.return_message(str(prepared_object))
        pass

    def real_resource(self, path):
        #doorpi.DoorPi().event_handler('OnWebServerRealResource', __name__, {'path': path})
        if os.path.isdir(self.server.www + path): return self.list_directory(self.server.www + path)
        try:
            return self.return_file_content(path)
        except IOError as exp:
            return self.real_resource_fallback(path, exp)
        except Exception as exp:
            logger.exception(exp)
            return self.send_error(500, str(exp))

    def real_resource_fallback(self, path, previous_exception):
        try:
            return self.return_fallback_content(self.server.online_fallback, path)
        except IOError:
            return self.send_error(404, str(previous_exception))
        except Exception as exp:
            logger.exception(exp)
            return self.send_error(500, str(exp))

    def list_directory(self, path):
        dirs = []
        files = []
        for item in os.listdir(path):
            if os.path.isfile(item): files.append(item)
            else: dirs.append(item)

        return_html = '''<!DOCTYPE html>
            <html lang="en"><head></head><body><a href="..">..</a></br>
        '''
        for dir in dirs: return_html += '<a href="./{dir}">{dir}</a></br>'.format(dir = dir)
        for file in files: return_html += '<a href="./{file}">{file}</a></br>'.format(file = file)
        return_html += "</body></html>"
        return self.return_message(return_html, 'text/html')
        #return self.send_error(403)

    def return_redirection(self, new_location):
        message = '''
        <html>
        <meta http-equiv="refresh" content="0;url={new_location}">
        <a href="{new_location}">{new_location}</a>
        </html>
        '''.format(new_location = new_location)
        return self.return_message(message, 'text/html', http_code = 301)

    @staticmethod
    def get_mime_typ(url):
        return guess_type(url)[0] or ""

    @staticmethod
    def is_file_parsable(filename):
        mime_type = DoorPiWebRequestHandler.get_mime_typ(filename)
        return mime_type in ['text/html']

    def read_from_file(self, url):
        try:
            read_mode = "r" if self.is_file_parsable(url) else "rb"
            with open(url, read_mode) as file:
                file_content = file.read()
            if self.is_file_parsable(url):
                return self.parse_content(file_content)
            else:
                return self.parse_content(file_content)
        except Exception as exp:
            raise exp

    def read_from_fallback(self, url):
        response = load_online_fallback(url, timeout = 1)
        if self.is_file_parsable(url):
            return self.parse_content(response.read(), True)
        else:
            return response.read()

    def get_file_content(self, path):
        content = mime = ""
        try:
            content = self.read_from_file(self.server.www + path)
            mime = self.get_mime_typ(self.server.www + path)
        except Exception as first_exp:
            try:
                logger.trace('use onlinefallback - local file  %s not found', self.server.www + path)
                content = self.read_from_fallback(self.server.online_fallback + path)
                mime = self.get_mime_typ(self.server.online_fallback + path)
            except Exception as exp:
                return self.send_error(404, str(first_exp)+" - "+str(exp))

        return content, mime

    def return_file_content(self, path):
        content, mime = self.get_file_content(path)
        return self.return_message(
            content, mime
        )

    def return_message(self, message = "", content_type = 'text/plain; charset=utf-8', http_code = 200,):
        self.send_response(http_code)
        #if login_form:
        self.send_header('WWW-Authenticate', 'Basic realm=\"%s\"' % doorpi.DoorPi().name_and_version)
        self.send_header("Server", doorpi.DoorPi().name_and_version)
        self.send_header("Content-type", content_type)
        self.send_header('Connection', 'close')
        self.end_headers()
        self.wfile.write(message)

    def login_form(self):
        try:
            login_form_content = self.read_from_file(self.server.www + "/" + self.server.loginfile)
        except IOError:
            logger.info('Missing login file: '+ self.server.loginfile)
            login_form_content = '''
                <head>
                <title>Error response</title>
                </head>
                <body>
                <h1>Error response</h1>
                <p>Error code 401.
                <p>Message: <a href="http://tools.ietf.org/html/rfc7235#section-3.1">RFC7235 - 401 Unauthorized</a>
                <p>Error code explanation: 401 = Unauthorized.
                </body>
            '''
        except Exception as exp:
            logger.exception(exp)
            self.send_error(500, str(exp))
            return False

        self.return_message(
            message = self.parse_content(login_form_content),
            content_type = 'text/html; charset=utf-8',
            http_code = 401
        )
        return True

    def authentication_required(self):
        parsed_path = urlparse(self.path)

        public_resources = self.conf.get_keys(self.server.area_public_name, log = False)
        for public_resource in public_resources:
            if re.match(public_resource, parsed_path.path):
                logger.debug('public resource: %s',parsed_path.path)
                return False

        try:
            username, password = self.headers['authorization'].replace('Basic ', '').decode('base64').split(':', 1)
        except Exception as exp:
            logger.debug('no header Authorization object (%s)', exp)
            return True

        user_session = self.server.sessions.get_session(username)
        if not user_session:
            user_session = self.server.sessions.build_security_object(username, password)

        if not user_session:
            logger.debug('need authentication (no session): %s', parsed_path.path)
            return True

        for write_permission in user_session['writepermissions']:
            if re.match(write_permission, parsed_path.path):
                logger.info('user %s has write permissions: %s', user_session['username'], parsed_path.path)
                return False

        for read_permission in user_session['readpermissions']:
            if re.match(read_permission, parsed_path.path):
                logger.info('user %s has read permissions: %s', user_session['username'], parsed_path.path)
                return False

        logger.warning('user %s has no permissions: %s', user_session['username'], parsed_path.path)
        return True

    def check_authentication(self):
        if not self.authentication_required(): return True
        raise WebServerLoginRequired()

    def create_mirror(self):
        parsed_path = urlparse(self.path)
        message_parts = [
                'CLIENT VALUES:',
                'client_address=%s (%s)' % (self.client_address,
                                            self.address_string()),
                'raw_requestline=%s' % self.raw_requestline,
                'command=%s' % self.command,
                'path=%s' % self.path,
                'real path=%s' % parsed_path.path,
                'query=%s' % parsed_path.query,
                'request_version=%s' % self.request_version,
                '',
                'SERVER VALUES:',
                'server_version=%s' % self.server_version,
                'sys_version=%s' % self.sys_version,
                'protocol_version=%s' % self.protocol_version,
                '',
                'HEADERS RECEIVED:',
        ]
        for name, value in sorted(self.headers.items()):
            message_parts.append('%s=%s' % (name, value.rstrip()))
        message_parts.append('')
        message = '\r\n'.join(message_parts)
        return message

    def parse_content(self, content, online_fallback = False, **mapping_table):
        try:
            matches = re.findall(r"{([^}\s]*)}", content)
            if not matches: return content
            #http://stackoverflow.com/questions/12897374/get-unique-values-from-a-list-in-python/12897491#12897491
            matches = list(set(matches))

            mapping_table['DOORPI'] =           doorpi.DoorPi().name_and_version
            mapping_table['SERVER'] =           self.server.server_name
            mapping_table['PORT'] =             str(self.server.server_port)
            mapping_table['MIN_EXTENSION'] =    '' if logger.getEffectiveLevel() <= 5 else '.min'

            #nutze den Hostnamen aus der URL. sonst ist ein erneuter Login nötig
            if 'host' in self.headers.keys():
                mapping_table['BASE_URL'] =     "http://%s"%self.headers['host']
            else:
                mapping_table['BASE_URL'] =     "http://%s:%s"%(self.server.server_name, self.server.server_port)

            # Trennung DATA_URL (AJAX) und BASE_URL (Dateien)
            mapping_table['DATA_URL'] = mapping_table['BASE_URL']

            if online_fallback and self.server.online_fallback:
                mapping_table['BASE_URL'] = self.server.online_fallback

            # Templates:
            mapping_table['TEMPLATE:HTML_HEADER'] =     'html.header.html'
            mapping_table['TEMPLATE:HTML_FOOTER'] =     'html.footer.html'
            mapping_table['TEMPLATE:NAVIGATION'] =      'navigation.html'

            for match in matches:
                if match not in mapping_table.keys(): continue
                if match.startswith('TEMPLATE:'):
                    try: replace_with = self.read_from_file(self.server.www + '/dashboard/parts/' + mapping_table[match])
                    except IOError:
                        if self.server.online_fallback:
                            replace_with = self.read_from_fallback(
                                self.server.online_fallback +
                                '/dashboard/parts/' +
                                mapping_table[match]
                            )
                        else:
                            replace_with = ""
                    except Exception: replace_with = ""
                    content = content.replace(
                        '{'+match+'}',
                        replace_with or ""
                    )
                else:
                    content = content.replace('{'+match+'}', mapping_table[match])

        except Exception as exp:
            logger.exception(exp)
        finally:
            return content