From bf88a8e19fb33646ef4f9388bd34cbfa3d8c73f1 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Mon, 14 Feb 2022 12:27:57 +0100 Subject: [PATCH 01/24] update README, install pycrypto as last package to avoid issues on windows --- simulation/README.md | 22 +++++++++++----------- simulation/requirements.txt | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/simulation/README.md b/simulation/README.md index c623237..6ee66d6 100644 --- a/simulation/README.md +++ b/simulation/README.md @@ -28,6 +28,17 @@ On Windows the package `pycryptodome>=3.14.0,<4` shall be used instead of Simulation webpages use [bootstrap 3.4][ref-bootstrap-34]. +## Usage + +Run the simulation of the ESP WiFi Manager **after** activating the virtual +environment of the [Setup section](#setup) + +```bash +sh run.sh +``` + +Open [`http://127.0.0.1:5000/`](http://127.0.0.1:5000/){:target="_blank"} in a browser + ## Unittests ### General @@ -170,17 +181,6 @@ Test [`wifi manager`][ref-wifi-manager-test] implementation. nose2 --config tests/unittest.cfg -v tests.test_wifi_manager.TestWiFiManager ``` -## Usage - -Run the simulation of the ESP WiFi Manager **after** activating the virtual -environment of the [Setup section](#setup) - -```bash -sh run.sh -``` - -Open [`http://127.0.0.1:5000/`](http://127.0.0.1:5000/){:target="_blank"} in a browser - [ref-bootstrap-34]: https://getbootstrap.com/docs/3.4/getting-started/#download diff --git a/simulation/requirements.txt b/simulation/requirements.txt index 219574c..71b02b0 100644 --- a/simulation/requirements.txt +++ b/simulation/requirements.txt @@ -2,5 +2,5 @@ flask>=2.0.2,<3 jinja2>=3.0.2,<4 PyYAML>=5.4.1,<6 nose2>=0.10.0,<1 -pycrypto>=2.6.1,<3 netifaces>=0.11.0,<1 +pycrypto>=2.6.1,<3 From d3d7ea28aa043dde0cf22ed527657c88cbe4512f Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Mon, 14 Feb 2022 12:30:33 +0100 Subject: [PATCH 02/24] remove debug output in simulation network to report dummy data usage --- simulation/src/wifi_helper/network.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/simulation/src/wifi_helper/network.py b/simulation/src/wifi_helper/network.py index f880ffa..7128eeb 100644 --- a/simulation/src/wifi_helper/network.py +++ b/simulation/src/wifi_helper/network.py @@ -78,8 +78,6 @@ def _scan_mac() -> List[dict]: scan_result = subprocess.check_output(['/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport', '--scan']) if len(scan_result) == 0: - print('### Check WiFi to be active ###') - print('Returning dummy data') return NetworkHelper._dummy_data() scan_result = [ele.decode('ascii') for ele in scan_result.splitlines()] @@ -188,17 +186,14 @@ def _scan_windows() -> List[dict]: try: scan_result = subprocess.check_output(['netsh', 'wlan', 'show', 'networks', 'mode=bssid']) except Exception as e: - print('Failed to scan due to this error: {}'.format(e)) - print('### Check WiFi to be active ###') - print('Returning dummy data') + # print('Failed to scan due to this error: {}'.format(e)) return NetworkHelper._dummy_data() # netsh call returns report in local language, try to decode it try: scan_result = [ele.decode('cp850').lstrip() for ele in scan_result.splitlines()] except Exception as e: - print('Failed to decode scan data due to this error: {}'.format(e)) - print('Returning dummy data') + # print('Failed to decode scan data due to this error: {}'.format(e)) return NetworkHelper._dummy_data() # [ @@ -366,6 +361,7 @@ def _dummy_data() -> List[dict]: 'hidden': False } ] + # print('Returning dummy data') return nets From c1cf894b9bbb0ce5b7690a80b04ab2ab31035e5d Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Mon, 14 Feb 2022 12:33:18 +0100 Subject: [PATCH 03/24] do not print message if authmode is unknown --- simulation/src/wifi_helper/wifi_helper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/simulation/src/wifi_helper/wifi_helper.py b/simulation/src/wifi_helper/wifi_helper.py index 03eedc1..6ee9792 100644 --- a/simulation/src/wifi_helper/wifi_helper.py +++ b/simulation/src/wifi_helper/wifi_helper.py @@ -341,7 +341,8 @@ def scan_networks(self) -> None: try: net['authmode'] = self.auth_modes[net['authmode']] except KeyError: - print('{} is unknown authmode'.format(net['authmode'])) + # print('{} is unknown authmode'.format(net['authmode'])) + pass except Exception: pass if 'bssid' in net: From 25fe41d4c5862e3bc185643a512254165ceafcc5 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Mon, 14 Feb 2022 12:43:09 +0100 Subject: [PATCH 04/24] add new pages, shorten URLs to available pages, extract functions from Flask functions --- simulation/src/wifi_manager/wifi_manager.py | 267 ++++++++++++-------- 1 file changed, 158 insertions(+), 109 deletions(-) diff --git a/simulation/src/wifi_manager/wifi_manager.py b/simulation/src/wifi_manager/wifi_manager.py index 55ad016..3b4225a 100644 --- a/simulation/src/wifi_manager/wifi_manager.py +++ b/simulation/src/wifi_manager/wifi_manager.py @@ -10,7 +10,7 @@ import gc import json from machine import machine -import os +# import os from pathlib import Path import _thread import time @@ -21,7 +21,7 @@ from generic_helper import Message # pip installed packages -from flask import Flask, render_template, abort, request, jsonify +from flask import Flask, render_template, url_for, redirect, request, jsonify # custom packages from generic_helper import GenericHelper @@ -65,11 +65,12 @@ def __init__(self, logger=None, quiet=False, name=__name__): self._enc_key = (uuid * amount).decode('ascii')[:required_len] self._configured_networks = list() + self._selected_network_bssid = '' # WiFi scan specific defines self._scan_lock = _thread.allocate_lock() self._scan_interval = 5000 # milliseconds - # Queue also works, but not in this case there is no need for a history + # Queue also works, but in this case there is no need for a history self._scan_net_msg = Message() self._scan_net_msg.set([]) # empty list, required by save_wifi_config self._latest_scan = None @@ -120,7 +121,8 @@ def load_and_connect(self) -> bool: # self._configured_networks = list(ssids).copy() # self.logger.debug('All SSIDs: {}'.format(ssids)) self.logger.debug('Config content: {}'.format(loaded_cfg)) - self.logger.debug('Private config content: {}'.format(private_cfg)) + self.logger.debug('Private config content: {}'. + format(private_cfg)) self.logger.debug('Configured networks: {}'. format(self._configured_networks)) @@ -152,8 +154,10 @@ def start_config(self) -> None: def _add_app_routes(self) -> None: """Add all application routes to the webserver.""" self.app.add_url_rule("/", view_func=self.landing_page) - self.app.add_url_rule("/wifi_selection", view_func=self.wifi_selection) - self.app.add_url_rule("/wifi_configs", view_func=self.wifi_configs) + self.app.add_url_rule("/select", view_func=self.wifi_selection) + self.app.add_url_rule("/render_network_inputs", + view_func=self.render_network_inputs) + self.app.add_url_rule("/configure", view_func=self.wifi_configs) self.app.add_url_rule("/save_wifi_config", view_func=self.save_wifi_config, methods=['POST', 'GET']) @@ -270,7 +274,8 @@ def extend_wifi_config_data(self, def _load_wifi_config_data(self, path: str, - encrypted: bool = False) -> Union[dict, List[dict]]: + encrypted: bool = False) -> Union[dict, + List[dict]]: """ Load WiFi configuration data from file. @@ -286,7 +291,8 @@ def _load_wifi_config_data(self, if encrypted: # read file in binary as it contains encrypted data - encrypted_read_data = GenericHelper.load_file(path=path, mode='rb') + encrypted_read_data = GenericHelper.load_file(path=path, + mode='rb') self.logger.debug('Read encrypted data: {}'. format(encrypted_read_data)) @@ -321,7 +327,6 @@ def _scan(self, msg: Message, scan_interval: int, lock: int) -> None: - # lock: lock) -> None: """ Scan for available networks. @@ -369,8 +374,8 @@ def scan_interval(self, value: int) -> None: """ Set the WiFi scan interval in milliseconds. - Values below 1000ms are set to 1000ms. - One scan takes around 3 sec, which leads to maximum 15 scans per minute + Values below 1000 ms are set to 1000 ms. + One scan takes around 3 sec, which leads to maximum 15 scans per min :param value: Interval of WiFi scans in milliseconds :type value: int @@ -423,7 +428,8 @@ def latest_scan(self) -> Union[List[dict], str]: # free = gc.mem_free() # self.logger.debug('Free memory: {}'.format(free)) latest_scan_result = self._scan_net_msg.value() - self.logger.info('Requested latest scan result: {}'.format(latest_scan_result)) + self.logger.info('Requested latest scan result: {}'. + format(latest_scan_result)) return latest_scan_result # ------------------------------------------------------------------------- @@ -436,19 +442,73 @@ def landing_page(self): # @app.route('/scan_result') def scan_result(self): + """Provide latest found networks as JSON""" return jsonify(self.latest_scan) - # @app.route('/wifi_selection') + # @app.route('/select') def wifi_selection(self): - # abort(404) + """ + Provide webpage to select WiFi network from list of available networks + + Scanning just in time of accessing the page would block all processes + for approx. 2.5 sec. + Using the result provided by the scan thread via a message takes only + 0.02 sec to complete + """ available_nets = self.latest_scan - # return render_template('wifi_select_loader.tpl.html', - return render_template('wifi_select_loader_bootstrap.tpl.html', - wifi_nets=available_nets) + content = self._render_network_inputs(available_nets=available_nets) + return render_template('select.tpl.html', content=content) + + # @app.route('/render_network_inputs') + def render_network_inputs(self) -> str: + """Return rendered network inputs content to webpage""" + available_nets = self.latest_scan + content = self._render_network_inputs(available_nets=available_nets) + return content + + def _render_network_inputs(self, available_nets: dict) -> str: + """ + Render HTML list of selectable networks + + :param available_nets: The available nets + :type available_nets: dict + + :returns: Sub content of WiFi selection page + :rtype: str + """ + content = "" + if len(available_nets): + for ele in available_nets: + selected = '' + if ele['bssid'] == self._selected_network_bssid: + selected = "checked" + self.logger.info('Mark BSSID {} as selected'. + format(ele['bssid'])) + content += """ + + + """.format(bssid=ele['bssid'], + state=selected, + ssid=ele['ssid'], + quality=ele['quality']) + else: + # as long as no networks are available show a spinner + content = """ +
+ Loading... +
+ """ + + return content - # @app.route('/wifi_configs') + # @app.route('/configure') def wifi_configs(self): - # abort(404) + """Provide webpage with table of configured networks""" configured_nets = self.configured_networks self.logger.debug('Existing config content: {}'. format(configured_nets)) @@ -456,37 +516,65 @@ def wifi_configs(self): if isinstance(configured_nets, str): configured_nets = [configured_nets] - return render_template('wifi_configs.tpl.html', - wifi_nets=configured_nets) + # disable submit button by default + return render_template('remove.tpl.html', + wifi_nets=configured_nets, + button_mode='disabled') # @app.route('/save_wifi_config', methods=['POST', 'GET']) def save_wifi_config(self): - # abort(404) - if request.method == 'POST': - data = request.form - form_data = dict(request.form) - # print('Posted data in save_wifi_config: {}'.format(data)) self.logger.info('WiFi user input content: {}'.format(form_data)) + # result if a network of the list has been selected # {'bssid': 'a0f3c1fbfc3c', 'ssid': '', 'password': 'sdsfv'} + # result if a custom network is given + # {'ssid': 'myCustom Net', 'password': 'asdf1234'} + # result if nothing is selected + # {'ssid': '', 'password': ''} + + self._save_wifi_config(form_data=form_data) - network_cfg = self._save_wifi_config(form_data=form_data) + return redirect(url_for('landing_page')) - # resp = jsonify(success=True) - # return resp - return render_template('result.html', result=network_cfg) + def _save_wifi_config(self, form_data: dict) -> None: + """ + Save a new WiFi configuration. - def _save_wifi_config(self, form_data: dict) -> dict: + :param form_data: The form data + :type form_data: dict + """ network_cfg = dict() available_nets = self.latest_scan self.logger.info('Available nets: {}'.format(available_nets)) - # [{'ssid': 'TP-LINK_FBFC3C', 'RSSI': -21, 'bssid': 'a0f3c1fbfc3c', 'authmode': 'WPA/WPA2-PSK', 'quality': 9, 'channel': 1, 'hidden': False}, {'ssid': 'FRITZ!Box 7490', 'RSSI': -17, 'bssid': '3810d517eb39', 'authmode': 'WPA2-PSK', 'quality': 27, 'channel': 11, 'hidden': False}] + # [ + # { + # 'ssid': 'TP-LINK_FBFC3C', + # 'RSSI': -21, + # 'bssid': 'a0f3c1fbfc3c', + # 'authmode': 'WPA/WPA2-PSK', + # 'quality': 9, + # 'channel': 1, + # 'hidden': False + # }, + # { + # 'ssid': 'FRITZ!Box 7490', + # 'RSSI': -17, + # 'bssid': '3810d517eb39', + # 'authmode': 'WPA2-PSK', + # 'quality': 27, + # 'channel': 11, + # 'hidden': False + # } + # ] # find SSID of network based on given bssid value if form_data['ssid'] != '': network_cfg['ssid'] = form_data['ssid'] else: + if 'bssid' not in form_data: + return + # selected_bssid = form_data['wifi_network'] selected_bssid = form_data['bssid'] for ele in available_nets: @@ -497,11 +585,11 @@ def _save_wifi_config(self, form_data: dict) -> dict: # JSON which does not handle bytes format this_bssid = str(ele['bssid']) else: - this_bssid = ele['bssid'] #.decode('ascii') + this_bssid = ele['bssid'] # .decode('ascii') if this_bssid == selected_bssid: # use string, json loading will fail otherwise later - network_cfg['ssid'] = ele['ssid'] #.decode('ascii') + network_cfg['ssid'] = ele['ssid'] # .decode('ascii') break network_cfg['password'] = form_data['password'] @@ -523,87 +611,48 @@ def _save_wifi_config(self, form_data: dict) -> dict: else: self.logger.info('No valid SSID found, will not save this net') - return network_cfg - # @app.route('/remove_wifi_config', methods=['POST', 'GET']) def remove_wifi_config(self): - # abort(404) - if request.method == 'POST': - data = request.args - - # Whether form data comes from GET or POST request, once parsed, - # it's available as req.form dictionary - form_data = request.get_json(force=True) - self.logger.info('Received data: {}'.format(form_data)) - # print('Posted data in remove_wifi_config: {}'.format(data)) - - if all(ele in form_data for ele in ['name', 'index']): - network_name = form_data['name'] - network_index = int(form_data['index']) - 1 # th is also counted - - self.logger.debug('Remove network "{}" at index {}'. - format(form_data['name'], form_data['index'])) - - if network_name == "all" and network_index == -1: - if PathHelper.exists(path=self._config_file): - os.remove(self._config_file) - self.logger.debug('Removed network file') - return + """Remove a configured WiFi network""" + form_data = dict(request.form) + + self.logger.info('Remove networks: {}'.format(form_data)) + # Remove networks: {'FRITZ!Box 7490': 'FRITZ!Box 7490'} + self._remove_wifi_config(form_data=form_data) + + return redirect(url_for('landing_page')) + + def _remove_wifi_config(self, form_data: dict) -> None: + """ + Remove a WiFi network from the configuration file. + + :param form_data: The form data + :type form_data: dict + """ + if len(form_data): loaded_cfg = self._load_wifi_config_data(path=self._config_file, encrypted=True) - self.logger.debug('Existing config content: {}'.format(loaded_cfg)) - try: - network_cfg = loaded_cfg[network_index] - if network_cfg['ssid'] == network_name: - self.logger.debug('Found specified network in network cfg') - - ssids = list() - if isinstance(loaded_cfg, list): - # remove element from list - del loaded_cfg[network_index] - - self.logger.debug('Updated data: {}'. - format(loaded_cfg)) - - # list of dicts - for net in loaded_cfg: - if 'ssid' in net: - ssids.append(net['ssid']) - elif isinstance(loaded_cfg, dict): - pass - # do not do anything, updated data will be empty list - - # if 'ssid' in loaded_cfg: - # ssids = loaded_cfg['ssid'] - - self._configured_networks = ssids.copy() - - # create bytes array of the dict and encrypt it - encrypted_data = self._encrypt_data(data=loaded_cfg) - - # save to file as binary - GenericHelper.save_file(data=encrypted_data, - path=self._config_file, - mode='w') # 'wb' for encrypted data - self.logger.debug('Saved encrypted data as json: {}'. - format(encrypted_data)) - except IndexError as e: - self.logger.debug('Specified network at index {} not found'. - format(network_index)) - except Exception as e: - self.logger.debug('Catched: {}'.format(e)) - - # resp = jsonify(success=True) - # return resp - return render_template('result.html', result=data) - - """ - @app.route('/result') - def show_result(): - return render_template('result.html', result=result) - """ + updated_cfg = list() + updated_ssids = list() + + for net in loaded_cfg: + if net['ssid'] not in form_data: + updated_cfg.append(net) + updated_ssids.append(net['ssid']) + + self._configured_networks = updated_ssids.copy() + + # create bytes array of the dict and encrypt it + encrypted_data = self._encrypt_data(data=updated_cfg) + + # save to file as binary + GenericHelper.save_file(data=encrypted_data, + path=self._config_file, + mode='wb') + self.logger.debug('Saved encrypted data as json: {}'. + format(encrypted_data)) def run(self, host: str = '0.0.0.0', From f147e682914696f637ab79f8ec68e00e43b57f35 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Mon, 14 Feb 2022 12:43:48 +0100 Subject: [PATCH 05/24] reduce index page to minimum content --- simulation/templates/index.tpl.html | 32 ++++++----------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/simulation/templates/index.tpl.html b/simulation/templates/index.tpl.html index 643a6e6..784afe2 100755 --- a/simulation/templates/index.tpl.html +++ b/simulation/templates/index.tpl.html @@ -5,38 +5,18 @@ Setup - + - -
-
-
-
- - From c1a8b58c4525f3182024fd9088d23336ee0d306e Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Mon, 14 Feb 2022 12:44:29 +0100 Subject: [PATCH 06/24] cleanup WiFi net remove template file --- simulation/templates/remove.tpl.html | 128 +++++++++++---------------- 1 file changed, 52 insertions(+), 76 deletions(-) diff --git a/simulation/templates/remove.tpl.html b/simulation/templates/remove.tpl.html index cd8dbc7..f0167fc 100644 --- a/simulation/templates/remove.tpl.html +++ b/simulation/templates/remove.tpl.html @@ -3,34 +3,48 @@ + Remove WiFi Network - + + - +
-
-