# Copyright 3D Control Systems, Inc. All Rights Reserved 2017-2019.
# Built in San Francisco.

# This software is distributed under a commercial license for personal,
# educational, corporate or any other use.
# The software as a whole or any parts of it is prohibited for distribution or
# use without obtaining a license from 3D Control Systems, Inc.

# All software licenses are subject to the 3DPrinterOS terms of use
# (available at https://www.3dprinteros.com/terms-and-conditions/),
# and privacy policy (available at https://www.3dprinteros.com/privacy-policy/)

import cgi
import http.server
import hashlib
import logging
import os
import simplejson as json
import time
import urllib.request, urllib.parse, urllib.error
try:
    import pam #apt-get install python-pampy or pacman -S python-pam on ArchLinuxARM
except:
    pam = None

import config
import joystick_interface
import log
import paths
import platforms
import rights
import user_login
import version


class WebInterfaceHandler(http.server.BaseHTTPRequestHandler):

    WRITE_CHUNK_SIZE = 16384
    URL = config.get_settings()['URL']

    @staticmethod
    def decode_url_to_unicode(url):
        return urllib.parse.unquote(url.decode('utf-8').replace("+", "%20"))

    def log_message(self, format, *args): # otherwise every request will be logged
        return

    def setup(self):
        self.logger = logging.getLogger(__name__)
        self.request.settimeout(12)
        http.server.BaseHTTPRequestHandler.setup(self)
        self.page_former = self.server.page_former
        self.host, _ = self.client_address[:2]

    def write_with_autoreplace(self, page, response=200, headers = {}):
        version_string = "v.%s.%s_%s" % (version.version, version.build, version.branch)
        is_text_page = type(page) == str
        if is_text_page: #binary data like images will have type bytes and doesn't need patching
            page = page.replace('!!!VERSION!!!', version_string)
            page = page.replace('!!!URL!!!', self.URL.replace('cli-cloud', 'cloud'))
            page = page.replace('!!!HOST!!!', self.host)
        if self.server.app.stop_flag:
            headers["Connection"] = "close"
        else:
            headers["Connection"] = "keep-alive"
        if is_text_page: # vital for correct length calculation
            length = len(page.encode("utf-8"))
        else:
            length = len(page)
        headers["Content-Length"] = length
        if not "Content-Type" in headers:
            headers["Content-Type"] = "text/html; charset=utf-8"
        try:
            self.send_response(response)
        except (ConnectionResetError, BrokenPipeError):
            self.logger.warning("Lost connection to browser")
        except Exception:
            self.logger.exception('Error while sending response on %s: ' % self.path)
        try:
            for keyword, value in headers.items():
                self.send_header(keyword, value)
            self.end_headers()
        except (ConnectionResetError, BrokenPipeError):
            self.logger.warning("Lost connection to browser")
        except Exception:
            self.logger.exception('Error while sending headers %s: ' % self.path)
        else:
            if is_text_page:
                page = page.encode('utf-8')
            chunk_start = 0
            while chunk_start < length:
                chunk_end = max(chunk_start + self.WRITE_CHUNK_SIZE, length)
                try:
                    self.wfile.write(page[chunk_start:chunk_end])
                except (ConnectionResetError, BrokenPipeError):
                    self.logger.warning("Lost connection to browser")
                    break
                except Exception:
                    self.logger.exception('Error while writing page to %s: ' % self.path)
                    break
                chunk_start = chunk_end

    def get_posted_data(self, *value_names):
        try:
            content_length = int(self.headers['Content-Length'])
        except Exception as e:
            self.logger.warning('Getting posted content failed: ' + str(e))
        else:
            parsed_posts = []
            if content_length:
                content_type, dict_to_parse = cgi.parse_header(self.headers['content-type'])
                if content_type == 'multipart/form-data':
                    result = []
                    for name in value_names:
                        result.append(cgi.parse_multipart(self.rfile, dict_to_parse)[name][0])
                    return result
                else:
                    body = self.rfile.read(content_length)
                    body = self.decode_url_to_unicode(body)
                    for index, name in enumerate(value_names):
                        name += '='
                        if index != 0 and parsed_posts:
                            name = '&' + name
                        if name in body:
                            parsed_post = body.split(name)[-1]
                            if index < len(value_names) - 1:
                                next_index = index + 1
                                while next_index < len(value_names):
                                    if value_names[next_index] in body:
                                        parsed_post = parsed_post.split('&' + value_names[next_index])[0]
                                        break
                                    next_index += 1
                            parsed_posts.append(parsed_post)
                            body = body.replace(name + parsed_post, '')
                if len(parsed_posts) == 1:
                    return parsed_posts[0]
                return parsed_posts

    def get_cookie(self, key):
        cookies = self.headers.get('Cookie', None)
        if cookies:
            cookies = cookies.split('; ')
            for cookie in cookies:
                if cookie.startswith(key):
                    return cookie.split('%s=' % key)[-1]

    def fix_cookie_name_https(self, key):
        if self.server.HTTPS:
            return key + '-https'
        return key

    def write_message(self, message, show_time=2, response=200, headers={}):
        page = self.server.web_content_files['templates']['pages']['message.html']
        page = page.replace('!!!MESSAGE!!!', message)
        if show_time:
            page = page.replace('!!!SHOW_TIME!!!', str(show_time))
        else:
            page = page.replace('<meta http-equiv="refresh" content="!!!SHOW_TIME!!!; url=/" />', '')
        self.write_with_autoreplace(page, response=response, headers=headers)

    def redirect(self, url):
        self.send_response(301)
        self.send_header('Location', url)
        self.end_headers()

    def path_contains(self, path):
        return self.path.find(path) >= 0

    def need_remote_login(self):
        if self.host != config.LOCAL_IP and self.server.app.user_login.user_token:
            token = self.get_cookie(self.fix_cookie_name_https('token'))
            return not self.check_cookie_token(token)

    @log.log_exception
    def do_GET(self):
        if self.path_contains('static'):
            self.get_static()
        elif self.need_remote_login():
            self.write_with_autoreplace(self.server.web_content_files['templates']['pages']['remote_login.html'])
        elif self.path_contains('ajax'):
            self.ajax_do_GET()
        elif self.path_contains('user_login'):
            self.process_login()
        elif self.path_contains('logout'):
            self.process_logout()
        elif self.path_contains('quit'):
            self.quit_main_app()
        elif self.path_contains('show_logs'):
            self.show_logs()
        elif self.path_contains('show_release_notes'):
            filename = os.path.join(os.path.dirname(__file__), "..", "release_notes.txt")
            with open(filename) as f:
                content = f.read()
            self.wfile.write(content.encode("utf-8"))
        elif self.path_contains('clear_logs'):
            self.clear_logs()
        elif self.path_contains('open_logs'):
            log.open_settings_folder()
            self.write_message(self.page_former.form_back_button('/'), show_time = 0)
        elif self.path_contains('kill_conveyor'):
            self.kill_conveyor()
        elif self.path_contains('connect_to_network_printer'):
            self.write_with_autoreplace(self.page_former.form_connect_to_network_printer_page())
        elif self.path_contains('detect_network_clients'):
            page = self.page_former.form_detect_network_clients_page()
            if page:
                self.write_with_autoreplace(page)
            else:
                self.write_message('No 3DPrinterOS Clients detected in your network')
        elif self.path_contains('edit_settings'):
            self.write_with_autoreplace(self.page_former.form_edit_settings_page())
        elif self.path_contains('update_from_file'):
            self.write_with_autoreplace(self.server.web_content_files['templates']['pages']['update_from_file.html'])
        elif self.path_contains('manage_settings'):
            self.write_with_autoreplace(self.page_former.form_manage_settings_page())
        elif self.path_contains('rpi_access_control'):
            if self.check_if_rpi():
                self.write_with_autoreplace(self.server.web_content_files['templates']['pages']['rpi_access_control.html'])
        elif self.path_contains('telemetry'):
            self.telemetry()
        elif self.path_contains('plugins_page'):
            self.write_with_autoreplace(self.server.web_content_files['templates']['pages']['plugins_page.html'])
        elif self.path_contains('check_host'):
            self.check_host()
        elif self.path_contains('check_makerware'):
            self.check_makerware()
        elif self.path_contains('check_linux_rights'):
            self.check_linux_rights()
        elif self.path_contains('check_if_printing'):
            self.check_if_printing()
        elif self.path_contains('telemetry'):
            self.telemetry()
        elif self.path_contains('network_printer_list_page'):
            self.write_with_autoreplace(self.server.web_content_files['templates']['pages']['network_printer_list.html'])
        elif self.path_contains('network_printers_list'):
            self.network_printers_list()
        elif self.path_contains('plugins_list'):
            self.plugins_list()
        elif self.path_contains('install_plugin_page'):
            self.write_with_autoreplace(self.server.web_content_files['templates']['pages']['install_plugin.html'])
        elif self.path_contains('/favicon.ico'):
            self.answer_with_image('favicon.ico')
        else:
            page = self.page_former.form_main_page()
            if page:
                self.write_with_autoreplace(page, headers={'Access-Control-Allow-Origin':'*'})
            else:
                self.write_message('Initializing...', show_time=1)

    def get_static(self):
        keys = self.path.split('/')[1:]
        web_content_files = dict(self.server.web_content_files)
        for key in keys:
            if key in web_content_files:
                if isinstance(web_content_files[key], dict):
                    web_content_files = web_content_files[key]
                elif isinstance(web_content_files[key], str):
                    if key.endswith('css'):
                        self.write_with_autoreplace(web_content_files[key], headers={'Content-Type': 'text/css'})
                    elif key.endswith('js'):
                        self.write_with_autoreplace(web_content_files[key], headers={'Content-Type': 'text/javascript'})
                    else:
                        self.write_with_autoreplace(web_content_files[key])
                elif isinstance(web_content_files[key], bytes):
                    if key.endswith('png'):
                        self.write_with_autoreplace(web_content_files[key], headers={'Content-Type': 'image/png'})
                    else:
                        self.logger.warning("Unknown web content binary file that is not png: %s" % key )
                else:
                    self.logger.warning('Unknown key while getting static files: ' + key)

    def ajax_do_GET(self):
        if self.path_contains('wizard'):
            self.ajax_wizard_do_GET()
        elif self.path_contains('get_printers_info'):
            self.write_with_autoreplace(self.page_former.get_printers_info(),
                                        headers={'Content-Type': 'text/html'})
        elif self.path_contains('get_updates_button'):
            if self.server.app and hasattr(self.server.app, 'updater') and self.server.app.updater.update_available:
                update_button_content = self.server.web_content_files['templates']['buttons']['get_updates_button.html']
            else:
                update_button_content = ''
            self.write_with_autoreplace(update_button_content, headers={'Content-Type': 'text/html'})
        elif self.path_contains('write_log_tail'):
            self.write_log_tail()
        elif self.path_contains('get_closing_client_info'):
            self.write_with_autoreplace(self.page_former.form_closing_message(), headers={'Content-Type': 'text/html'})
        elif self.path_contains('check_login'):
            is_login = bool(hasattr(self.server.app, "user_login") and self.server.app.user_login.login)
            response = json.dumps({'result': is_login})
            self.write_with_autoreplace(response, headers={'Content-Type':'application/json'})

    def ajax_wizard_do_GET(self):
        if self.path_contains('get_printers_info_without_controls'):
            self.write_with_autoreplace(self.page_former.get_printers_info(False, False, False, False))
        elif self.path_contains('get_printers_info_with_type_button'):
            self.write_with_autoreplace(self.page_former.get_printers_info(False, True, False, False))
        elif self.path_contains('get_printers_info_with_rename_button'):
            self.write_with_autoreplace(self.page_former.get_printers_info(True, False, False, False))
        elif self.path_contains('get_printers_info_with_workgroups_button'):
            self.write_with_autoreplace(self.page_former.get_printers_info(False, False, True, False))

    @log.log_exception
    def do_POST(self):
        if self.path_contains('remote_login'):
            self.process_remote_login()
        elif self.need_remote_login():
            self.write_with_autoreplace(self.server.web_content_files['templates']['pages']['remote_login.html'])
        elif self.path_contains('ajax'):
            self.ajax_do_POST()
        elif self.path_contains('joystick'):
            self.joystick_do_POST()
        elif self.path_contains('wizard'):
            self.wizard_do_POST()
        elif self.path_contains('login'):
            self.process_login()
        elif self.path_contains('quit_confirmation'):
            self.write_with_autoreplace(self.page_former.form_quit_confirm_page())
        elif self.path_contains('quit'):
            self.quit_main_app()
        elif self.path_contains('logout_confirmation'):
            self.write_with_autoreplace(self.page_former.form_logout_confirm_page())
        elif self.path_contains('logout'):
            self.process_logout()
        elif self.path_contains('kill_conveyor'):
            self.kill_conveyor()
        elif self.path_contains('add_user_groups'):
            self.add_user_groups()
        elif self.path_contains('ignore_groups_warning'):
            self.ignore_groups_warning()
        elif self.path_contains('get_updates'):
            self.get_updates()
        elif self.path_contains('update_software'):
            self.update_software()
        elif self.path_contains('choose_cam'):
            self.choose_cam()
        elif self.path_contains('switch_camera'):
            self.switch_camera()
        elif self.path_contains('open_cloudsync_folder'):
            self.open_cloudsync_folder()
        elif self.path_contains('choose_printer_type'):
            self.choose_printer_type()
        elif self.path_contains('confirm_printer_groups'):
            self.confirm_printer_groups()
        elif self.path_contains('report_problem'):
            self.report_problem()
        elif self.path_contains('send_problem_report'):
            self.send_problem_report()
        elif self.path_contains('restart_camera'):
            self.server.app.camera_controller.restart_camera()
            self.write_message('Camera module restarted')
        elif self.path_contains('start_hd_camera'):
            self.start_hd_camera()
        elif self.path_contains('set_new_printer_name'):
            self.set_new_printer_name()
        elif self.path_contains('toggle_virtual_printer'):
            self.toggle_virtual_printer()
        elif self.path_contains('reset_printer_type'):
            self.reset_printer_type()
        elif self.path_contains('restore_default_settings'):
            self.write_with_autoreplace(self.server.web_content_files['templates']['pages']['restore_default_settings.html'])
        elif self.path_contains('confirm_default_settings_restoring'):
            config.Config.instance().restore_default_settings()
            self.write_message('Default settings restored. Please restart app to apply changes.')
        elif self.path_contains('toggle_cloud_sync'):
            self.toggle_cloud_sync()
        elif self.path_contains('tray'):
            self.tray_do_POST()
        elif self.path_contains('process_network_printer'):
            self.process_network_printer()
        elif self.path_contains('confirm_detect_network_printers'):
            self.write_with_autoreplace(self.server.web_content_files['templates']['pages']
                                        ['confirm_detect_printers.html'])
        elif self.path_contains('detect_network_printers'):
            self.server.app.network_printers_detector.network_detector_run_flag = True
            self.write_message('<p>Detecting network printers...</p>'
                               'If you have a printer connected to your network<br />it will appear in printers list',
                               show_time=4)
        elif self.path_contains('process_pairing_network_printer'):
            self.do_GET()
        elif self.path_contains('forget_network_printer'):
            self.forget_network_printer()
        elif self.path_contains('save_settings'):
            self.save_settings()
        elif self.path_contains('settings'):
            self.settings_do_POST()
        elif self.path_contains('accept_update_from_file'):
            self.accept_update_from_file()
        elif self.path_contains('control_plugin'):
            self.control_plugin()
        elif self.path_contains('upload_plugin'):
            self.accept_and_install_plugin_file()
        elif self.path_contains('change_ssh_password'):
            self.change_ssh_password()
        elif self.path_contains('generate_ssh_keys'):
            self.generate_ssh_keys()
        elif self.path_contains('revoke_all_ssh_keys'):
            self.revoke_all_ssh_keys()
        elif self.path_contains('add_public_key'):
            self.add_public_key()
        else:
            self.write_message('Not found', 0, 404)

    def display_wizard_stage(self, stage_name):
        self.page_former.set_wizard_stage(stage_name)
        self.do_GET()

    def settings_do_POST(self):
        settings = config.get_settings()
        if self.path_contains('toggle_update'):
            settings['update']['enabled'] = not settings['update']['enabled']
            config.Config.instance().save_settings(settings)
            self.write_with_autoreplace(self.page_former.form_manage_settings_page())
        elif self.path_contains('toggle_auto_update'):
            settings['update']['auto_update_enabled'] = not settings['update']['auto_update_enabled']
            config.Config.instance().save_settings(settings)
            self.write_with_autoreplace(self.page_former.form_manage_settings_page())
        elif self.path_contains('set_update_check_pause'):
            try:
                check_pause = int(self.get_posted_data('check_pause'))
            except:
                self.write_message('Setting update check pause failed: wrong value!')
            else:
                settings['update']['check_pause'] = int(check_pause)
                config.Config.instance().save_settings(settings)
                self.write_with_autoreplace(self.page_former.form_manage_settings_page())

    def wizard_do_POST(self):
        if self.path_contains('greetings'):
            self.page_former.wizard_enabled = True
            self.display_wizard_stage('greetings')
        elif self.path_contains('close_wizard'):
            self.close_wizard()
        elif self.path_contains('connect_printer'):
            self.display_wizard_stage('connect_printer')
        elif self.path_contains('choose_printer_type'):
            self.display_wizard_stage('choose_printer_type')
        elif self.path_contains('choose_live_view_mode'):
            self.display_wizard_stage('choose_live_view_mode')
        elif self.path_contains('tray_icon'):
            self.display_wizard_stage('tray_icon')
        elif self.path_contains('more_features'):
            self.display_wizard_stage('more_features')
        elif self.path_contains('rename_printer'):
            self.display_wizard_stage('rename_printer')
        elif self.path_contains('printer_workgroups'):
            self.display_wizard_stage('printer_workgroups')
        elif self.path_contains('cloud_sync'):
            self.display_wizard_stage('cloud_sync')
        elif self.path_contains('restore_default_settings'):
            self.display_wizard_stage('restore_default_settings')
        elif self.path_contains('report_problem'):
            self.display_wizard_stage('report_problem')
        elif self.path_contains('final'):
            self.display_wizard_stage('final')
        else:
            self.write_message('Wizard stage not found', 0, 404)

    def ajax_do_POST(self):
        if self.path_contains('rename_printer'):
            self.rename_printer()
        elif self.path_contains('select_printer_groups'):
            self.select_printer_groups()
        elif self.path_contains('select_printer_type'):
            self.select_printer_type()
        elif self.path_contains('manage_printer'):
            self.manage_printer()
        elif self.path_contains('request_printer_type_reset'):
            self.request_printer_type_reset()
        elif self.path_contains('toggle_local_mode'):
            self.toggle_local_mode()
        elif self.path_contains('cancel_printing'):
            self.cancel_print()
        else:
            self.write_message('Not found', 0, 404)

    def tray_do_POST(self):
        response = None
        if self.path_contains('get_active_printers'):
            response = self.page_former.form_json_active_printers()
        elif self.path_contains('get_not_ready_printers'):
            response = self.page_former.form_json_active_printers(include_ready=False)
        elif self.path_contains('confirm_identical_printers'):
            response = self.page_former.form_json_confirm_identical_printers()
        elif self.path_contains('updates_available'):
            response = self.page_former.form_json_updates_available()
        if response:
            self.write_with_autoreplace(response.encode("utf-8"))

    def toggle_virtual_printer(self):
        self.server.app.toggle_virtual_printer()
        self.do_GET()

    def rename_printer(self):
        usb_info = self.get_posted_data('usb_info')
        page = self.page_former.form_rename_printer_page(usb_info)
        if page:
            self.write_with_autoreplace(page)
        else:
            self.write_message('Printer not found. Probably it was disconnected.')

    def set_new_printer_name(self):
        printer_name, usb_info = self.get_posted_data('printer_name', 'usb_info')
        printer_interface = self.server.find_printer_interface(usb_info)
        if printer_interface:
            if printer_name:
                printer_interface.request_printer_rename(printer_name)
                self.write_message('Printer rename requested: "' + printer_name +'"')
            else:
                self.write_message('Wrong printer name')
        else:
            self.write_message('Printer not found. Probably it was disconnected.')

    def select_printer_groups(self):
        usb_info = self.get_posted_data('usb_info')
        page = self.page_former.form_printer_groups_page(usb_info)
        if page:
            self.write_with_autoreplace(page)
        else:
            if config.get_settings()['protocol']['encryption']:
                prefix = 'https://'
            else:
                prefix = 'http://'
            back_button = self.page_former.form_back_button('/')
            self.write_message("<p>To create a new Workgroup Access code, please check Admin tab \
                                at <a href='" + prefix + \
                               "!!!URL!!!/workgroup' target='_blank'>!!!URL!!!/workgroup</a></p>\
                                Workgroup access codes is a premium \
                                feature, to use that please contact our sales: \
                                <a href='mailto:info@3dprinteros.com'>info@3dprinteros.com</a>" \
                               + back_button , show_time=0)

    def confirm_printer_groups(self):
        usb_info = self.get_unicode_string_from_current_path('/confirm_printer_groups/')
        printer_interface = self.server.find_printer_interface(usb_info)
        if printer_interface:
            printer_groups = printer_interface.get_groups()
            check_list = []
            for index in range(0, len(printer_groups)):
                check_list.append('selected_group_id_' + str(index))
            selected_groups_id = self.get_posted_data(*check_list)
            if type(selected_groups_id) != list:
                selected_groups_id = [] if not selected_groups_id else [str(selected_groups_id)]
            for index, group in enumerate(printer_groups):
                printer_groups[index]['active'] = str(group['id']) in selected_groups_id
            printer_interface.request_printer_groups_selection(printer_groups)
            self.write_message('Groups for ' + printer_interface.printer_profile['name'] + ' successfully updated')
        else:
            self.write_message('Printer not found. Probably it was disconnected.')

    def find_printer_type(self, printer_interface, type_name):
        types_to_select = getattr(printer_interface, "possible_printer_types", None)
        if types_to_select:
            for printer_type in types_to_select:
                if printer_type['name'] == type_name:
                    return printer_type

    def select_printer_type(self):
        usb_info = self.get_posted_data('usb_info')
        page = self.page_former.form_type_select_page(usb_info)
        if page:
            self.write_with_autoreplace(page)
        else:
            self.write_message('Error while displaying printer types')

    def choose_printer_type(self):
        printer_type, usb_info = self.get_posted_data('printer_type', 'usb_info')
        printer_interface = self.server.find_printer_interface(usb_info)
        if printer_interface:
            if printer_type:
                chosen_type = self.find_printer_type(printer_interface, printer_type)
                printer_interface.request_printer_type_selection(chosen_type)
                message = 'Printer type selected:  ' + str(printer_type)
            else:
                message = 'Printer type not selected'
        else:
            message = 'Printer not found. Probably it was disconnected.'
        self.write_message(message)

    def request_printer_type_reset(self):
        usb_info = self.get_posted_data('usb_info')
        page = self.page_former.form_type_reset_page(usb_info)
        if page:
            self.write_with_autoreplace(page)
        else:
            self.write_message('Printer not found. Probably it was disconnected.')

    def get_unicode_string_from_current_path(self, substring_to_remove=''):
        return self.decode_url_to_unicode(self.path.replace(substring_to_remove, ''))

    def accept_update_from_file(self):
        try:
            file_data = self.get_posted_data('package')[0]
        except Exception as e:
            message = 'Failed to accept update package! %s' % str(e)
            self.logger.warning(message)
            self.write_message(message)
        else:
            with open(paths.UPDATE_FILE_PATH, 'wb') as f:
                f.write(file_data)
            self.write_with_autoreplace(self.server.web_content_files['templates']['pages']
                                        ['update_from_file_accepted.html'])

    def process_remote_login(self):
        try:
            login, password = self.get_posted_data('login', 'password')
        except:
            self.write_message('Authorization failed! Wrong email or password.')
        else:
            if login and password and self.check_login_and_password(login, password):
                token = self.create_cookie_token()
                header_value = '%s=%s; HttpOnly' % (self.fix_cookie_name_https('token'), token)
                if self.server.HTTPS:
                    header_value += '; Secure'
                self.write_message('Login successful!<br><br>Processing...', headers= {'Set-cookie': header_value})
            else:
                self.write_message('Authorization failed! Wrong email or password.')

    def save_settings(self):
        settings = self.get_posted_data('settings')
        try:
            settings = json.loads(settings)
            config.Config.instance().save_settings(settings)
        except Exception as e:
            self.write_message('Error while saving settings<br>%s' % str(e))
        else:
            self.write_message('Settings successfully saved.<br>But will be applied only on after restart!', show_time=5)

    def joystick_do_POST(self):
        if self.path_contains('start'):
            self.start_joystick(self.get_posted_data('printer_id'))
        elif self.path_contains('send_command'):
            printer_id, command, value = self.get_posted_data('printer_id', 'command', 'value')
            joystick_interface.execute_command(self.server.find_printer_interface(printer_id), command, value)
            self.write_with_autoreplace('OK')
        elif self.path_contains('get_printer_info'):
            self.send_printer_info(self.get_posted_data('printer_id'))
        elif self.path_contains('send_gcode'):
            printer_id, gcode_string = self.get_posted_data('printer_id', 'gcode_string')
            joystick_interface.send_gcode(self.server.find_printer_interface(printer_id), gcode_string)
            self.write_with_autoreplace('OK')

    def send_printer_info(self, printer_id):
        printer_interface = self.server.find_printer_interface(printer_id)
        self.write_with_autoreplace(json.dumps(joystick_interface.get_printers_info(printer_interface)),
                                    headers={'Content-Type': 'application\json'})

    def start_joystick(self, printer_id):
        printer_interface = self.server.find_printer_interface(printer_id)
        if printer_interface and printer_interface.printer:
            if not printer_interface.local_mode:
                #self.write_with_autoreplace(self.page_former.form_local_mode_warning_page(printer_id))
                self.write_message("Please enable local mode for this printer in main screen", show_time=3)
            else:
                joystick_interface.init_callback(self.server.find_printer_interface(printer_id))
                self.write_with_autoreplace(self.page_former.form_joystick_page(printer_id))
        else:
            self.write_message('Printer not found. Probably it was disconnected.')

    def process_network_printer(self):
        printer_ip, printer_type = self.get_posted_data('printer_ip', 'printer_type')
        if printer_ip and printer_type:
            self.server.app.network_printers_detector.connect_to_network_printer(printer_ip, printer_type)
            self.write_message('Printer was added ' + printer_ip)
        else:
            self.write_message('Printer IP and type must not be empty!')

    def toggle_local_mode(self):
        printer_interface = self.server.find_printer_interface(self.get_posted_data('usb_info'))
        if printer_interface:
            if printer_interface.local_mode:
                self.logger.info("Local mode off")
                printer_interface.turn_off_local_mode()
            else:
                self.logger.info("Local mode on")
                printer_interface.turn_on_local_mode()
            self.send_response(200)
        else:
            self.write_message('Printer not found. Probably it was disconnected.')

    def cancel_print(self):
        printer_id = self.get_posted_data('usb_info')
        if printer_id:
            try:
                printer_interface = self.server.find_printer_interface(printer_id)
                printer_interface.printer.cancel()
                printer_interface.register_error(117, "Canceled locally", is_blocking=True)
            except:
                self.logger.debug("Fail to cancel print job for %s" % printer_id)

    def manage_printer(self):
        usb_info = self.get_posted_data('usb_info')
        page = self.page_former.form_manage_printer_page(usb_info)
        if page:
            self.write_with_autoreplace(page)
        else:
            self.write_message('Error while displaying printer managing page')

    def close_wizard(self, silent=False):
        settings = config.get_settings()
        settings['wizard_on_start'] = False
        config.Config.instance().save_settings(settings)
        self.page_former.wizard_enabled = False
        if not silent:
            self.write_message('Wizard successfully closed. Loading main page...')

    def toggle_cloud_sync(self):
        settings = config.get_settings()
        if self.server.app.cloud_sync_controller.cloud_sync_process:
            self.server.app.cloud_sync_controller.stop_cloud_sync_process()
            settings['cloud_sync']['enabled'] = False
        else:
            self.server.app.cloud_sync_controller.start_cloud_sync_process()
            settings['cloud_sync']['enabled'] = True
        config.Config.instance().save_settings(settings)
        self.redirect('manage_settings')

    def reset_printer_type(self):
        usb_info = self.get_posted_data('usb_info')
        printer_interface = self.server.find_printer_interface(usb_info)
        if printer_interface:
            printer_interface.request_reset_printer_type()
            self.write_message('Printer type reset attempt... Please retry on fail')
        else:
            self.write_message('Printer not found. Probably it was disconnected.')

    def open_cloudsync_folder(self):
        self.server.app.cloud_sync_controller.open_cloud_sync_folder()
        self.do_GET()

    def choose_cam(self):
        page = self.page_former.form_choose_cam_page()
        if page:
            self.write_with_autoreplace(page)
        else:
            self.write_message('Live view feature disabled')

    def switch_camera(self):
        module_name = str(self.get_posted_data('module'))
        if module_name:
            if module_name == self.server.app.camera_controller.HD_CAMERA_NAME:
                page = self.page_former.form_hd_camera_start_page()
                if page:
                    self.write_with_autoreplace(page)
                else:
                    self.write_message("Please connect your printer and select it's type to stream with HD camera module")
                return
            self.server.app.camera_controller.switch_camera(new_camera_name=module_name,
                                                            token=self.server.app.user_login.user_token)
            message = 'Live view mode switched to ' + module_name
        else:
            message = 'Live view mode not chosen'
        self.write_message(message)

    def start_hd_camera(self):
        printer_token = self.get_posted_data('hd_camera_printer_token')
        module_name = self.server.app.camera_controller.HD_CAMERA_NAME
        if printer_token:
            self.server.app.camera_controller.switch_camera(new_camera_name=module_name, token=printer_token)
            message = 'Live view mode switched to ' + module_name
        else:
            message = 'Error while starting camera module: ' + module_name
        self.write_message(message)

    def get_updates(self):
        self.write_with_autoreplace(self.page_former.form_get_updates_page())

    def update_software(self):
        result = self.server.app.updater.download_update()
        if result:
            self.write_message(result)
        else:
            self.write_with_autoreplace(self.page_former.form_update_downloaded_page())

    def show_logs(self):
        self.write_with_autoreplace(self.page_former.form_show_logs_page())

    def write_log_tail(self):
        logs = log.get_file_tail(log.LOG_FILE)
        content = ''
        for line in logs:
            content = content + line + '<br />'
        if not content:
            content = 'No logs'
        self.write_with_autoreplace(content)

    def ignore_groups_warning(self):
        self.server.app.rights_checker_waiter.ignore_flag = True
        time.sleep(self.server.app.rights_checker_waiter.CHECK_PERIOD)
        self.do_GET()

    def add_user_groups(self):
        self.server.app.rights_checker_waiter.add_user_groups()
        self.quit_main_app()

    def kill_conveyor(self):
        self.server.app.conveyor_kill_waiter.kill()
        if not self.server.app.conveyor_kill_waiter.waiting:
            message = 'Conveyor was successfully stopped.<br /><br />Returning...'
        else:
            message = '3DPrinterOS was unable to stop MakerBot conveyor service..'
        self.write_message(message)

    def report_problem(self):
        self.write_with_autoreplace(self.page_former.form_report_problem_page())

    def send_problem_report(self):
        summary, description = self.get_posted_data('problem_summary', 'problem_description')
        report_text = 'Summary:\n' + summary + '\n\nDescription:\n' + description
        error = log.report_problem(report_text)
        if error:
            self.write_message('Reporting problem failed!<br />' + error)
        else:
            self.write_message('Thank you! Your problem was successfully reported.')

    def clear_logs(self):
        log.clear_logs()
        self.write_message('Logs successfully cleared. Writing logs stopped. \
                           <br /><br />Please restart 3DPrinterOS Client to start writing new logs!'
                           + self.page_former.form_back_button('/'), show_time=0)

    def quit_main_app(self):
        self.write_with_autoreplace(self.page_former.form_closing_page())
        self.server.app.stop_flag = True

    def process_login(self):
        login_object = self.server.app.user_login
        if login_object.user_token:
            if login_object.login == self.parse_login()[0]:
                self.answer_on_login(True, 'You are already logged in')
                return
            else:
                error = (0, 'Please logout first before login as other user')
        else:
            login, password, token, force_normal_login = self.parse_login()
            if (login and password) or token:
                error = login_object.login_as_user(login, password, token)
            else:
                error = (1, "Login and password should not be empty")
        if error:
            self.answer_on_login(False, "Login error: " + error[1])
        else:
            if self.command == 'GET':
                #TODO seem to be magic, need to be refactored to clarify
                self.page_former.show_your_account_button = False
                config.get_settings()['camera']['enabled'] = False
                config.get_settings()['web_interface']['browser_opening_on_start'] = False
                self.close_wizard(silent=True)
            self.answer_on_login(True, 'Login successful!<br><br>Processing...')

    def parse_login(self):
        login, password, token = None, None, None
        force_normal_login = False
        if self.command == 'POST':
            login, password = self.get_posted_data('login', 'password')
            password = hashlib.sha256(password.encode('utf-8')).hexdigest()
        elif self.command == 'GET':
            query = urllib.parse.urlparse(self.path).query
            args = urllib.parse.parse_qs(query)
            login = args['user_login'][0]
            if 'auth_token' in args:
                token = args['auth_token'][0]
            elif 'password_hash' in args:
                password = args['password_hash'][0]
            elif 'password' in args:
                password = args['password'][0]
                password = hashlib.sha256(password.encode('utf-8')).hexdigest()
            force_normal_login = "force_normal_login" in args
        return login, password, token, force_normal_login

    def need_image_response(self):
        query = urllib.parse.urlparse(self.path).query
        args = urllib.parse.parse_qs(query)
        return 'image_answer' in args

    def answer_on_login(self, success, message):
        if self.need_image_response():
            if success:
                self.answer_with_image('success.jpg')
            else:
                self.answer_with_image('fail.jpg')
        else:
            self.write_message(message)

    def check_host(self):
        query = urllib.parse.urlparse(self.path).query
        args = urllib.parse.parse_qs(query)
        host_mac = args.get('check_host')[0]
        login_obj = self.server.app.user_login
        if login_obj.user_token:
            if host_mac.lower() == login_obj.mac.lower():
                self.answer_with_image('success.jpg')
                return
        self.answer_with_image('fail.jpg')

    def check_makerware(self):
        if self.server.app.conveyor_kill_waiter.waiting:
            self.answer_with_image('success.jpg')
        else:
            self.answer_with_image('fail.jpg')

    def check_if_printing(self):
        for pi in self.server.app.printer_interfaces:
            if pi.get_printer_state() in ('printing', 'paused', 'downloading'):
                self.answer_with_image('success.jpg')
                return
        self.answer_with_image('fail.jpg')

    def check_linux_rights(self):
        if self.server.app.rights_checker_waiter.waiting:
            self.answer_with_image('success.jpg')
        else:
            self.answer_with_image('fail.jpg')

    def answer_with_image(self, img_filename):
        #this function performs hack that necessary for sending requests from HTTPS to HTTP
        #answering with image is the only way to communicate between HTTPS and HTTP
        message = self.server.web_content_files['static']['images'][img_filename]
        if img_filename.endswith('png'):
            mime = 'image/png'
        elif img_filename.endswith('ico'):
            mime = 'image/ico'
        else:
            mime = 'image/jpeg'
        self.write_with_autoreplace(message, headers = { 'Content-Type': mime })

    def check_login_and_password(self, login, password):
        login_object = user_login.UserLogin(self.server.app, auto_login = False)
        login_object.login_as_user(login, hashlib.sha256(password.encode('utf-8')).hexdigest(), save_password_flag = False)
        if self.server.app.user_login.user_token == login_object.user_token:
            return True
        # check login using RPi's root credentials
        if platforms.get_platform() == 'rpi' and pam:
            try:
                return pam.authenticate(login, password, service='sudo') #service sudo is essential for this to work on RPi
            except:
                self.logger.exception("PAM authentication error")

    def create_cookie_token(self):
        md5hash = hashlib.md5()
        md5hash.update(self.server.app.user_login.login.encode("utf-8"))
        md5hash.update(self.server.app.user_login.user_token.encode("utf-8"))
        md5hash.update(time.strftime('%Y%m%d').encode("utf-8"))
        return md5hash.hexdigest()

    def check_cookie_token(self, token):
        return token == self.create_cookie_token()

    def check_if_rpi(self):
        if platforms.get_platform() == 'rpi':
            return True
        else:
            self.write_message('SSH access configuration available only on RaspberryPi', show_time = 3)

    def generate_ssh_keys(self):
        if self.check_if_rpi():
            private_key_path = rights.generate_ssh_keys()
            if private_key_path:
                headers = {}
                headers['Content-Type'] = 'application/octet-stream'
                headers['Content-Disposition'] = 'attachment; filename=' + os.path.basename(private_key_path)
                headers['Content-Transfer-Encoding'] = 'binary'
                headers['Accept-Ranges'] = 'bytes'
                with open(private_key_path) as f:
                    key = f.read()
                self.send_response(200)
                for field_name, value in list(headers.items()):
                    self.send_header(field_name, value)
                self.end_headers()
                self.wfile.write(bytes(key, "utf-8"))
                os.remove(private_key_path)
            else:
                message = 'An error occurred during keys generation.'
                self.write_message(message, show_time=5)

    def revoke_all_ssh_keys(self):
        if self.check_if_rpi():
            if rights.revoke_all_ssh_keys():
                message = 'All SSH key are revoked. Now image can only be accessed using password.'
            else:
                message = 'An error occurred during keys revoking.'
            self.write_message(message, show_time=5)

    def add_public_key(self):
        if self.check_if_rpi():
            try:
                public_key = self.get_posted_data('key')[0]
            except Exception as e:
                message = 'Failed to accept public key file! %s' % str(e)
                self.logger.warning(message)
                self.write_message(message)
            else:
                if rights.add_public_key(public_key):
                    message = 'SSH key was added to authorized keys. You can access image using it.'
                else:
                    message = 'An error occurred during the key addition.'
                self.write_message(message, show_time=5)

    def change_ssh_password(self):
        if self.check_if_rpi():
            password = self.get_posted_data('password')
            if password is not None:
                if rights.change_ssh_password(password):
                    message = "SSH password for root had been updated."
                else:
                    message = 'An error occurred during password updating.'
                self.write_message(message, show_time=5)

    def telemetry(self):
        app = self.server.app
        telemetry = {}
        telemetry['login'] = getattr(getattr(app, 'user_login', None), 'login', None)
        telemetry['version'] = version.version
        telemetry['update_available'] = app.updater.update_available
        telemetry['camera_type'] = app.camera_controller.current_camera_name
        telemetry['printers'] = [{'usb_info': pi.usb_info, 'report': pi.state_report()} for pi in app.printer_interfaces]
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps(telemetry).encode("utf-8"))

    def network_printers_list(self):
        printers_list = self.server.app.network_printers_detector.printers_list_with_profiles()
        printers_list = json.dumps(printers_list)
        self.logger.info("Network printers list:\t" + printers_list)
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(printers_list.encode("utf-8"))

    def forget_network_printer(self):
        usb_info_json = self.get_posted_data('usb_info')
        self.logger.info("Forgetting network printer: " + str(usb_info_json))
        usb_info = json.loads(usb_info_json)
        self.server.app.network_printers_detector.forget(usb_info)
        self.send_response(200)
        self.end_headers()

    def process_logout(self):
        user_login.logout()
        self.server.app.restart_flag = True
        self.quit_main_app()

    def plugins_list(self):
        plugins_list = list(self.server.app.plugin_controller.plugins.values())
        plugins_list = json.dumps(plugins_list)
        self.logger.info("Printers list:\t" + plugins_list)
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(plugins_list.encode("utf-8"))

    def control_plugin(self):
        command = self.get_posted_data('command')
        command = json.loads(command)
        action = command['action']
        method_name = action + "_plugin"
        method = getattr(self.server.app.plugin_controller, method_name, None)
        self.send_response(200)
        self.end_headers()
        if method:
            method(command["plugin_name"])
        else:
            raise RuntimeError("No such plugin action: " + method_name)

    def accept_and_install_plugin_file(self):
        try:
            file_data = self.get_posted_data('package')[0]
        except Exception as e:
            message = 'Failed to accept plugin package! %s' % str(e)
            self.logger.warning(message)
            self.write_message(message)
        else:
            path_to_save_file = os.path.join(paths.current_settings_folder(), "plugin_to_install.zip")
            with open(path_to_save_file, 'wb') as f:
                f.write(file_data)
            name = self.server.app.plugin_controller.install(path_to_save_file)
            self.server.app.plugin_controller.load_plugin(name)
            os.remove(path_to_save_file)
            self.write_with_autoreplace(self.server.web_content_files['templates']['pages']['plugins_page.html'])
