From 4f9717a2d3a3807bff221c9c321d3d7223f3eb78 Mon Sep 17 00:00:00 2001 From: David Escalona Date: Mon, 5 Sep 2022 17:38:04 +0200 Subject: [PATCH] connectcore-demo-example: add network configuration section Signed-off-by: David Escalona --- connectcore-demo-example/demoserver.py | 301 ++++++++++- connectcore-demo-example/index.html | 10 +- connectcore-demo-example/management.html | 22 +- connectcore-demo-example/multimedia.html | 22 +- connectcore-demo-example/network.html | 508 ++++++++++++++++++ .../static/css/general.css | 153 +++++- connectcore-demo-example/static/js/common.js | 9 + .../static/js/dashboard.js | 2 - connectcore-demo-example/static/js/devices.js | 5 + connectcore-demo-example/static/js/network.js | 459 ++++++++++++++++ 10 files changed, 1465 insertions(+), 26 deletions(-) create mode 100644 connectcore-demo-example/network.html create mode 100644 connectcore-demo-example/static/js/network.js diff --git a/connectcore-demo-example/demoserver.py b/connectcore-demo-example/demoserver.py index 0d74695..8c3570f 100755 --- a/connectcore-demo-example/demoserver.py +++ b/connectcore-demo-example/demoserver.py @@ -29,10 +29,10 @@ import socketserver import stat import subprocess -from digi.apix.bluetooth import BluetoothException, BluetoothDevice +from digi.apix.bluetooth import BluetoothException, BluetoothDevice, BluetoothProfile from digi.apix.exceptions import DigiAPIXException -from digi.apix.network import NetworkException, NetStatus, NetworkInterface -from digi.apix.wifi import WifiException, WifiInterface +from digi.apix.network import IPMode, NetworkException, NetStatus, NetworkInterface, NetworkProfile +from digi.apix.wifi import SecurityMode, WifiException, WifiInterface, WifiProfile from logging.handlers import SysLogHandler from subprocess import call, TimeoutExpired from threading import Thread @@ -56,6 +56,13 @@ ZERO_MAC = "00:00:00:00:00:00" ZERO_IP = "0.0.0.0" NOT_AVAILABLE = "N/A" +ELEMENT_BLUETOOTH = "bluetooth" +ELEMENT_ETHERNET = "ethernet" +ELEMENT_WIFI = "wifi" + +PREFIX_ETHERNET = "eth" +PREFIX_WIFI = "wlan" + # Variables. log = logging.getLogger(APP_NAME) last_cpu_work = 0 @@ -210,7 +217,7 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler): # Fill bluetooth devices data. try: - bt_devices = BluetoothDevice.list_devices() + bt_devices = BluetoothDevice.list_devices() or [] for device in bt_devices: try: bt_device = BluetoothDevice.get(device) @@ -226,7 +233,7 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler): # Fill ethernet interfaces data. try: - interfaces = NetworkInterface.list_interfaces() + interfaces = NetworkInterface.list_interfaces() or [] for iface in interfaces: try: net_iface = NetworkInterface.get(iface) @@ -242,7 +249,7 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler): # Fill WiFi interfaces data. try: - interfaces = WifiInterface.list_interfaces() + interfaces = WifiInterface.list_interfaces() or [] for iface in interfaces: try: wifi_iface = WifiInterface.get(iface) @@ -567,7 +574,34 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler): progress = 100 if fw_process and fw_process.poll() is None: progress = "?" - self.wfile.write(json.dumps({"progress": progress, "message": "Updating firmare"}).encode(encoding="utf_8")) + self.wfile.write(json.dumps({"progress": progress, "message": "Updating firmware"}).encode(encoding="utf_8")) + elif re.search("/ajax/get_config", self.path) is not None: + # Set the response headers. + self._set_headers(200) + # Get the JSON data. + data = self.rfile.read(int(self.headers["Content-Length"])) + elements = json.loads(data.decode("utf-8")).get("elements", None) + # Fill configuration data. + config_data = get_elements_configuration(elements) + # Send the answer. + if "error" in config_data: + self.wfile.write(json.dumps(config_data).encode(encoding="utf_8")) + else: + self.wfile.write(json.dumps( + {"data": json.dumps(config_data)}).encode(encoding="utf_8")) + elif re.search("/ajax/set_config", self.path) is not None: + # Set the response headers. + self._set_headers(200) + # Get the JSON data. + data = self.rfile.read(int(self.headers["Content-Length"])) + configuration = json.loads(data.decode("utf-8")).get("configuration", None) + # Apply configuration. + result = set_configuration(configuration) + # Send the answer. + if "error" in result: + self.wfile.write(json.dumps(result).encode(encoding="utf_8")) + else: + self.wfile.write(json.dumps({"data": json.dumps(result)}).encode(encoding="utf_8")) else: # Forbidden. self._set_headers(403) @@ -612,7 +646,7 @@ def filter_by_extension(name, filters): if name.endswith(ext): return True - return False; + return False def get_uptime(): @@ -945,6 +979,257 @@ def set_audio_volume(value): return None +def get_elements_configuration(elements): + """ + Gets the configuration for the given elements. + + Args: + elements (List): Elements for which to get the configuration. + + Returns: + Dictionary: The elements configuration data. + """ + data = {} + for element in elements: + if element == ELEMENT_BLUETOOTH: + data[element] = get_bluetooth_element_configuration() + elif element in (ELEMENT_ETHERNET, ELEMENT_WIFI): + data[element] = get_network_element_configuration(element) + else: + data["error"] = f"Unknown element '{element}'" + + return data + + +def get_network_element_configuration(element): + """ + Gets the configuration for the given network element. + + Args: + element (String): Network element for which to get the configuration. + + Returns: + Dictionary: The network element configuration data. + """ + data = {} + prefix = PREFIX_ETHERNET + if element == ELEMENT_WIFI: + prefix = PREFIX_WIFI + try: + ifaces = NetworkInterface.list_interfaces() or [] + for iface in ifaces: + if prefix in iface: + data[iface] = get_network_iface_configuration(iface) + except DigiAPIXException as exc: + data["error"] = f"Error listing network interfaces: '{str(exc)}'" + + return data + + +def get_network_iface_configuration(interface): + """ + Returns the network configuration of the given interface. + + Args: + interface (String): Network interface to get its configuration. + + Returns: + Dictionary: dictionary with the interface configuration. + """ + data = {} + try: + net_iface = NetworkInterface.get(interface) + data["enable"] = 1 if net_iface.status == NetStatus.CONNECTED else 0 + data["mac"] = mac_to_human_string(net_iface.mac) + data["type"] = 0 if net_iface.ip_mode == IPMode.STATIC else 1 + data["ip"] = str(net_iface.ipv4) + data["netmask"] = str(net_iface.netmask) + data["dns1"] = str(net_iface.dns1) + data["dns2"] = str(net_iface.dns2) + data["gateway"] = str(net_iface.gateway) + if PREFIX_WIFI in interface: + try: + wifi_iface = WifiInterface.get(interface) + data["ssid"] = wifi_iface.ssid + data["frequency"] = wifi_iface.frequency + data["channel"] = wifi_iface.channel + data["sec_mode"] = 0 if wifi_iface.sec_mode == SecurityMode.UNKNOWN \ + else wifi_iface.sec_mode.code + except WifiException as exc: + raise NetworkException({str(exc)}) from exc + except NetworkException as exc2: + data["error"] = f"Error reading interface data: {str(exc2)}" + + return data + + +def set_configuration(config_data): + """ + Applies the given configuration. + + Args: + config_data (Dictionary): Configuration to apply. + + Returns: + Dictionary: The operation result. + """ + data = {} + for element in config_data: + if element == ELEMENT_BLUETOOTH: + data[element] = set_bluetooth_element_configuration(config_data[element]) + elif element in (ELEMENT_ETHERNET, ELEMENT_WIFI): + data[element] = set_network_element_configuration(config_data[element]) + else: + data["error"] = f"Unknown element '{element}'" + + return data + + +def set_network_element_configuration(config_data): + """ + Applies the given network configuration. + + Args: + config_data (Dictionary): Network configuration to apply. + + Returns: + Dictionary: The operation result. + """ + data = {} + for iface in config_data: + data[iface] = set_network_iface_configuration(iface, config_data[iface]) + + return data + + +def set_network_iface_configuration(interface, config_data): + """ + Applies the given network configuration to the given interface. + + Args: + interface (String): Network interface to apply configuration to. + config_data (Dictionary): Network configuration to apply. + + Returns: + Dictionary: Operation result. + """ + data = {} + profile = NetworkProfile() + if PREFIX_WIFI in interface: + profile = WifiProfile() + profile.connect = config_data.get("enable", None) + profile.mode = IPMode.get(config_data["type"]) if "type" in config_data else None + profile.ipv4 = config_data.get("ip", None) + profile.netmask = config_data.get("netmask", None) + profile.gateway = config_data.get("gateway", None) + profile.dns1 = config_data.get("dns1", None) + profile.dns2 = config_data.get("dns2", None) + if PREFIX_WIFI in interface: + profile.ssid = config_data.get("ssid", None) + profile.sec_mode = (SecurityMode.get(config_data["sec_mode"]) + if "sec_mode" in config_data else None) + profile.psk = config_data.get("psk", None) + try: + if PREFIX_ETHERNET in interface: + net_iface = NetworkInterface.get(interface) + elif PREFIX_WIFI in interface: + net_iface = WifiInterface.get(interface) + else: + raise DigiAPIXException(f"Unknown interface '{interface}'") + net_iface.configure(profile) + data["status"] = 0 + except DigiAPIXException as exc2: + data["status"] = 1 + data["desc"] = f"Error configuring interface: {str(exc2)}" + + return data + + +def get_bluetooth_element_configuration(): + """ + Gets the configuration for the bluetooth element. + + Returns: + Dictionary: The bluetooth element configuration data. + """ + data = {} + try: + bt_devices = BluetoothDevice.list_devices() or [] + for bt_device in bt_devices: + data[bt_device] = get_bluetooth_device_configuration(bt_device) + except DigiAPIXException as exc: + data["error"] = f"Error listing bluetooth devices: '{str(exc)}'" + + return data + + +def get_bluetooth_device_configuration(device): + """ + Returns the configuration of the given bluetooth device. + + Args: + device (String): Bluetooth device to get its configuration. + + Returns: + Dictionary: dictionary with the device configuration. + """ + data = {} + try: + bt_device = BluetoothDevice.get(device) + data["device_id"] = bt_device.device_id + data["advert_name"] = str(bt_device.advert_name) + data["mac"] = mac_to_human_string(bt_device.mac) + data["enable"] = 1 if bt_device.is_enabled else 0 + data["running"] = 1 if bt_device.is_running else 0 + except BluetoothException as exc: + data["error"] = f"Error reading device data: {str(exc)}" + + return data + + +def set_bluetooth_element_configuration(config_data): + """ + Applies the given bluetooth configuration. + + Args: + config_data (Dictionary): Bluetooth configuration to apply. + + Returns: + Dictionary: The operation result. + """ + data = {} + for device in config_data: + data[device] = set_bluetooth_device_configuration(device, config_data[device]) + + return data + + +def set_bluetooth_device_configuration(device, config_data): + """ + Applies the given bluetooth configuration to the given device. + + Args: + device (String): Bluetooth device to apply configuration to. + config_data (Dictionary): Bluetooth configuration to apply. + + Returns: + Dictionary: Operation result. + """ + data = {} + try: + bt_device = BluetoothDevice.get(device) + profile = BluetoothProfile() + profile.enable = config_data.get("enable", None) + profile.advert_name = config_data.get("advert_name", None) + bt_device.configure(profile) + data["status"] = 0 + except DigiAPIXException as exc2: + data["status"] = 1 + data["desc"] = f"Error configuring interface: {str(exc2)}" + + return data + + def mac_to_human_string(mac, num_bytes=6): """ Transforms the given MAC address into a human readable string. diff --git a/connectcore-demo-example/index.html b/connectcore-demo-example/index.html index 7b63352..aa331c3 100644 --- a/connectcore-demo-example/index.html +++ b/connectcore-demo-example/index.html @@ -34,7 +34,6 @@ Digi Demo - Dashboard - +
  • + +
    + + Network +
    +
    +
  • @@ -293,6 +300,7 @@ Digi Demo - Management + diff --git a/connectcore-demo-example/multimedia.html b/connectcore-demo-example/multimedia.html index 27282fc..eaf617b 100644 --- a/connectcore-demo-example/multimedia.html +++ b/connectcore-demo-example/multimedia.html @@ -34,13 +34,12 @@ Digi Demo - Multimedia
    - - @@ -69,6 +68,14 @@ Digi Demo - Multimedia
  • +
  • + +
    + + Network +
    +
    +
  • @@ -234,6 +241,7 @@ Digi Demo - Multimedia + diff --git a/connectcore-demo-example/network.html b/connectcore-demo-example/network.html new file mode 100644 index 0000000..231e82f --- /dev/null +++ b/connectcore-demo-example/network.html @@ -0,0 +1,508 @@ + + + + +Digi Demo - Network + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + +
    + Loading... +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    No
    +
    Yes
    +
    +
    +
    +
    +
    + + Ethernet 0 +
    +
    +
    +
    Configure ethernet parameters.
    +
    + MAC Address: + +
    +
    + Connected: + +
    +
    + IP mode: + +
    +
    + IP address: + +
    +
    +
    + Subnet mask: + +
    +
    +
    + Default gateway: + +
    +
    +
    + DNS1 address: + +
    +
    +
    + DNS2 address: + +
    +
    +
    +
    Refresh
    +
    Save
    +
    +
    +
    +
    +
    + + Ethernet 1 +
    +
    + +
    +
    +
    + + Wi-Fi +
    +
    + +
    +
    +
    +
    + + + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + diff --git a/connectcore-demo-example/static/css/general.css b/connectcore-demo-example/static/css/general.css index 04c351e..cb27134 100644 --- a/connectcore-demo-example/static/css/general.css +++ b/connectcore-demo-example/static/css/general.css @@ -373,7 +373,7 @@ body { /* POPUP */ .popup { z-index: 999; - position: absolute; + position: fixed; transform: translate(-50%, -50%); background-color: white; padding: 20px 10px; @@ -1895,6 +1895,11 @@ body { width: 170px; } +.param-value { + display: inline-block; + position: relative; +} + .input-control { position: relative; -webkit-transition: none !important; @@ -1905,12 +1910,99 @@ body { width: 80px !important; padding: 0px !important; margin: 0px !important; + text-align: left !important; + padding-left: 5px !important; +} + +.input-control-wide { + width: 180px !important; } .input-control-error { background-color: #f3b1b1 !important; } +.input-control-disabled { + background-color: transparent !important; + border: none !important; + pointer-events: none !important; + color: #939393 !important; +} + +.select-control { + position: relative; + -webkit-transition: none !important; + -moz-transition: none !important; + -ms-transition: none !important; + -o-transition: none !important; + transition: none !important; + width: 80px !important; + padding: 0px !important; + margin: 0px !important; + text-align-last: left !important; + padding-left: 5px !important; +} + +.switch-control { + position: relative; + display: inline-block; + width: 48px; + height: 22px; + margin-bottom: 0px; +} + +.switch-control input { + opacity: 0; + width: 0; + height: 0; +} + +.slider-control { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.slider-control:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 3px; + bottom: 3px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .slider-control { + background-color: #2196F3; +} + +input:focus + .slider-control { + box-shadow: 0 0 1px #2196F3; +} + +input:checked + .slider-control:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +.slider-control.round { + border-radius: 11px; +} + +.slider-control.round:before { + border-radius: 50%; +} + .error-label { color: red; font-size: 12px; @@ -1919,6 +2011,11 @@ body { padding-left: 10px; } +.buttons-container { + with: 100%; + position: relative; +} + .config-button { padding: 5px; cursor: pointer; @@ -1935,6 +2032,43 @@ body { background-color: #c3c3c3; pointer-events: none; } + +input[type=text], input[type=password], select { + background-color: #f6f6f6; + border: none; + color: #0d0d0d; + padding: 12px 30px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 14px; + margin: 0px 0px 25px 0px; + width: 85%; + border: 2px solid #f6f6f6; + -webkit-transition: all 0.5s ease-in-out; + -moz-transition: all 0.5s ease-in-out; + -ms-transition: all 0.5s ease-in-out; + -o-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out; + -webkit-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; +} + +input[type=text]:focus, input[type=password]:focus, select:focus { + background-color: #fff; + border-bottom: 2px solid var(--digi-green); +} + +select { + -moz-appearance:none; /* Firefox */ + -webkit-appearance:none; /* Safari and Chrome */ + appearance:none; + text-align-last: center; + background-image: url("../images/combo_arrow_gray.png"); + background-repeat: no-repeat, repeat; + background-position: right .7em top 50%; + background-size: .65em auto; +} /* END CONFIG CONTROLS */ /* MANAGEMENT */ @@ -2206,3 +2340,20 @@ body { margin-top: 5px; } /* END MANAGEMENT */ + +/* NETWORK */ +.interface-header { + padding: 15px 20px 10px 20px; + align-items: center; + font-size: 16px; + font-weight: bold; +} + +.interface-panel-container { + padding: 0px 20px 15px 20px; +} + +.interface-title { + font-size: 1.25em; +} +/* END NETWORK */ diff --git a/connectcore-demo-example/static/js/common.js b/connectcore-demo-example/static/js/common.js index 54246bc..140cf5b 100644 --- a/connectcore-demo-example/static/js/common.js +++ b/connectcore-demo-example/static/js/common.js @@ -42,6 +42,7 @@ const ID_CPU_TEMPERATURE = "cpu_temperature"; const ID_CPU_UPTIME = "cpu_uptime"; const ID_CURRENT_DIR = "current_dir"; const ID_DATA = "data"; +const ID_DESC = "desc"; const ID_DEVICE_NAME = "device-name"; const ID_DEVICE_TYPE = "device_type"; const ID_DEVICES = "devices"; @@ -133,9 +134,12 @@ const VALUE_SUCCESSFUL = "successful"; const VALUE_TOP = "top"; const VALUE_UNKNOWN = "unknown"; +const CLASS_ARROW_DOWN = "fa-caret-down"; +const CLASS_ARROW_UP = "fa-caret-up"; const CLASS_D_NONE = "d-none"; const CLASS_DISABLED_DIV = "disabled-div"; const CLASS_ELEMENT_GRAYED = "element-grayed"; +const CLASS_INPUT_DISABLED = "input-control-disabled"; const CLASS_INPUT_ERROR = "input-control-error"; const CLASS_CONFIG_BUTTON_DISABLED = "config-button-disabled"; const CLASS_SELECTED = "selected"; @@ -487,6 +491,11 @@ function isDashboardShowing() { return window.location.pathname.indexOf("index") > -1; } +// Returns whether the network page is showing or not. +function isNetworkShowing() { + return window.location.pathname.indexOf("network") > -1; +} + // Returns whether the management page is showing or not. function isManagementShowing() { return window.location.pathname.indexOf("management") > -1; diff --git a/connectcore-demo-example/static/js/dashboard.js b/connectcore-demo-example/static/js/dashboard.js index ceb539a..2261599 100644 --- a/connectcore-demo-example/static/js/dashboard.js +++ b/connectcore-demo-example/static/js/dashboard.js @@ -92,8 +92,6 @@ const STREAM_BT_STATE = PREFIX_STREAM + IFACE_BT + "/state"; const PANEL_ARROW_WIDTH_100 = 20; const PANEL_BOARD_WIDTH_100 = 1200; -const CLASS_ARROW_DOWN = "fa-caret-down"; -const CLASS_ARROW_UP = "fa-caret-up"; const CLASS_LED_PANEL_AREA_ON = "led-panel-area-on"; const CLASS_PANEL_AREA_SELECTED = "panel-area-selected"; const CLASS_PANEL_AREA_ICON_SELECTED = "panel-area-icon-selected"; diff --git a/connectcore-demo-example/static/js/devices.js b/connectcore-demo-example/static/js/devices.js index c52cc40..146bed5 100644 --- a/connectcore-demo-example/static/js/devices.js +++ b/connectcore-demo-example/static/js/devices.js @@ -325,6 +325,11 @@ class ConnectCoreDevice { return this.#ethernetIP[index]; } + // Returns whether the device supports Wifi or not. + hasWifi() { + return this.#wifiMAC != null && this.#wifiMAC != "undefined" + } + // Returns the device WiFi MAC address. getWifiMAC() { return this.#wifiMAC; diff --git a/connectcore-demo-example/static/js/network.js b/connectcore-demo-example/static/js/network.js new file mode 100644 index 0000000..6fd2869 --- /dev/null +++ b/connectcore-demo-example/static/js/network.js @@ -0,0 +1,459 @@ +/* + * Copyright 2022, Digi International Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +// Constants. +const ELEMENT_ETHERNET = "ethernet"; +const ELEMENT_WIFI = "wifi"; + +const FIELD_DEFAULT_GATEWAY = "_default_gateway"; +const FIELD_DNS1_ADDRESS = "_dns1_addr"; +const FIELD_DNS2_ADDRESS = "_dns2_addr"; +const FIELD_ENABLE = "_enable"; +const FIELD_ENCRYPTION_TYPE = "_enc_type"; +const FIELD_ERROR = "_error"; +const FIELD_MAC_ADDRESS = "_mac"; +const FIELD_IP_ADDRESS = "_ip_addr"; +const FIELD_IP_MODE = "_ip_mode"; +const FIELD_PARAM = "_param"; +const FIELD_PASSWORD = "_password"; +const FIELD_SAVE_BUTTON = "_save_button"; +const FIELD_SSID = "_ssid"; +const FIELD_SUBNET_MASK = "_subnet_mask"; + +const ID_DNS1 = "dns1"; +const ID_DNS2 = "dns2"; +const ID_ENABLE = "enable"; +const ID_ETH0_TITLE = "eth0_title"; +const ID_GATEWAY = "gateway"; +const ID_IP = "ip"; +const ID_IP_MODE = "type"; +const ID_INTERFACE = "interface"; +const ID_MAC = "mac"; +const ID_NETMASK = "netmask"; +const ID_PANEL_CONTAINER = "_panel_container"; +const ID_PASSWORD = "psk"; +const ID_SECURITY_MODE = "sec_mode"; +const ID_SSID = "ssid"; +const ID_TOGGLE_BUTTON = "_toggle_button"; + +const IP_MODE_DHCP = "dhcp"; +const IP_MODE_STATIC = "static"; + +const ERROR_IP_VALUE_FORMAT = "Value must follow this format: XXX.XXX.XXX.XXX"; +const ERROR_PASSWORD_INVALID = "Password must be between 8 and 63 characters long"; +const ERROR_SSID_INVALID = "SSID value is not valid"; +const ERROR_UNKNOWN = "Unknown error saving configuration."; + +const MESSAGE_LOADING_CONFIGURATION = "Loading configuration..."; +const MESSAGE_SAVING_CONFIGURATION = "Saving configuration..."; +const MESSAGE_CONFIGURATION_SAVED = "Configuration saved successfully!"; + +const PREFIX_ETHERNET = "eth"; +const PREFIX_WIFI = "wlan"; + +const REGEX_IP = '^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'; +const REGEX_SSID = '^[^!#;+\\]/"\t][^+\\]/"\t]{0,31}$'; +const REGEX_PASSWORD = '^[\u0020-\u007e]{8,63}$'; + +const ALL_ELEMENTS = "all"; + +// Variables. +var readingNetworkInfo = false; +var passwordChanged = false; + +// Initializes the network page. +function initializeNetworkPage() { + // Sanity checks. + if (!isNetworkShowing()) + return; + // Read network configuration. + readElements = [ELEMENT_ETHERNET, ELEMENT_WIFI]; + readConfiguration(readElements, ALL_ELEMENTS); +} + +// Gets the device configuration the given elements. +function readConfiguration(readElements, parseElement) { + // Execute only in the network page. + if (!isNetworkShowing() || readingNetworkInfo) + return; + // Flag reading variable. + readingNetworkInfo = true; + // Hide the info popup. + showInfoPopup(false); + // Show the loading popup. + showLoadingPopup(true, MESSAGE_LOADING_CONFIGURATION); + // Send request to retrieve configuration. + $.post( + "http://" + getServerAddress() + "/ajax/get_config", + JSON.stringify({ + "elements": readElements + }), + function(data) { + // Process only in the network page. + if (!isNetworkShowing()) + return; + // Process answer. + processReadConfigurationResponse(data, parseElement); + } + ).fail(function(response) { + // Flag reading variable. + readingNetworkInfo = false; + // Clear the element being read. + elementReading = ""; + // Process only in the network page. + if (!isNetworkShowing()) + return; + // Hide the loading panel. + showLoadingPopup(false); + // Process error. + processAjaxErrorResponse(response); + // Update controls. + updateAllControls(); + }); +} + +// Processes the response of the read configuration request. +function processReadConfigurationResponse(response, parseElement) { + // Check if there was any error in the request. + if (!checkErrorResponse(response, false)) { + // Fill network info. + fillNetworkInfo(JSON.parse(response[ID_DATA]), parseElement); + } else { + // Flag reading variable. + readingNetworkInfo = false; + // Hide the loading panel. + showLoadingPopup(false); + } +} + +// Fills network information. +function fillNetworkInfo(response, parseElement) { + var numEthernetIfaces = 0; + var numWifiIfaces = 0; + for (const [element, elementData] of Object.entries(response)) { + if (element != ELEMENT_ETHERNET && element != ELEMENT_WIFI) + continue; + for (const [iface, ifaceData] of Object.entries(elementData)) { + if (!iface.startsWith(PREFIX_WIFI) && !iface.startsWith(PREFIX_ETHERNET)) + continue; + if (parseElement != ALL_ELEMENTS && iface != parseElement) + continue; + var enabled = ifaceData[ID_ENABLE]; + var ipMode = ifaceData[ID_IP_MODE]; + if (iface.startsWith(PREFIX_ETHERNET)) + numEthernetIfaces += 1; + else if (iface.startsWith(PREFIX_WIFI)) + numWifiIfaces += 1; + // Fill MAC. + document.getElementById(iface + FIELD_MAC_ADDRESS).innerText = ifaceData[ID_MAC]; + // Set enable state. + document.getElementById(iface + FIELD_ENABLE).checked = enabled; + // Set IP mode. + if (ipMode == 0) + document.getElementById(iface + FIELD_IP_MODE).value = IP_MODE_STATIC; + else + document.getElementById(iface + FIELD_IP_MODE).value = IP_MODE_DHCP; + // Fill the rest of the fields. + document.getElementById(iface + FIELD_IP_ADDRESS).value = ifaceData[ID_IP]; + document.getElementById(iface + FIELD_SUBNET_MASK).value = ifaceData[ID_NETMASK]; + document.getElementById(iface + FIELD_DEFAULT_GATEWAY).value = ifaceData[ID_GATEWAY]; + document.getElementById(iface + FIELD_DNS1_ADDRESS).value = ifaceData[ID_DNS1]; + document.getElementById(iface + FIELD_DNS2_ADDRESS).value = ifaceData[ID_DNS2]; + // Specific Wifi fields. + if (iface.startsWith(PREFIX_WIFI)) { + document.getElementById(iface + FIELD_SSID).value = ifaceData[ID_SSID]; + document.getElementById(iface + FIELD_ENCRYPTION_TYPE).selectedIndex = ifaceData[ID_SECURITY_MODE]; + if (ifaceData[ID_SECURITY_MODE] != 0) + document.getElementById(iface + FIELD_PASSWORD).value = "dummy_password"; + passwordChanged = false; + } + // Update controls. + updateInterfaceControls(iface); + } + } + // Update interfaces visibility. + if (parseElement == ALL_ELEMENTS) { + if (numEthernetIfaces == 1) { + document.getElementById(IFACE_ETH1).style.display = "none"; + document.getElementById(ID_ETH0_TITLE).innerHTML = "Ethernet"; + } + if (numWifiIfaces == 0) + document.getElementById(IFACE_WIFI).style.display = "none"; + } + // Flag reading variable. + readingNetworkInfo = false; + // Hide the loading panel. + showLoadingPopup(false); +} + +// Updates all controls of the page. +function updateAllControls() { + updateInterfaceControls(IFACE_ETH0); + updateInterfaceControls(IFACE_ETH1); + updateInterfaceControls(IFACE_WIFI); +} + +// Updates the given interface controls based on its configuration. +function updateInterfaceControls(iface) { + // Initialize variables. + var ipGroup = document.getElementById(iface + FIELD_IP_ADDRESS + FIELD_PARAM); + var ipField = document.getElementById(iface + FIELD_IP_ADDRESS); + var subnetGroup = document.getElementById(iface + FIELD_SUBNET_MASK + FIELD_PARAM); + var subnetField = document.getElementById(iface + FIELD_SUBNET_MASK); + var gatewayGroup = document.getElementById(iface + FIELD_DEFAULT_GATEWAY + FIELD_PARAM); + var gatewayField = document.getElementById(iface + FIELD_DEFAULT_GATEWAY); + var dns1Group = document.getElementById(iface + FIELD_DNS1_ADDRESS + FIELD_PARAM); + var dns1Field = document.getElementById(iface + FIELD_DNS1_ADDRESS); + var dns2Group = document.getElementById(iface + FIELD_DNS2_ADDRESS + FIELD_PARAM); + var dns2Field = document.getElementById(iface + FIELD_DNS2_ADDRESS); + var ipModeGroup = document.getElementById(iface + FIELD_IP_MODE + FIELD_PARAM); + var ipModeField = document.getElementById(iface + FIELD_IP_MODE); + var enableField = document.getElementById(iface + FIELD_ENABLE); + var enabled = enableField.checked; + var ipMode = ipModeField.value; + var ssidGroup, encryptionTypeGroup, encryptionTypeField, passwordGroup, usePassword; + if (iface.startsWith(PREFIX_WIFI)) { + ssidGroup = document.getElementById(iface + FIELD_SSID + FIELD_PARAM); + encryptionTypeGroup = document.getElementById(iface + FIELD_ENCRYPTION_TYPE + FIELD_PARAM); + encryptionTypeField = document.getElementById(iface + FIELD_ENCRYPTION_TYPE); + passwordGroup = document.getElementById(iface + FIELD_PASSWORD + FIELD_PARAM); + usePassword = encryptionTypeField.value != 0; + if (usePassword) + passwordGroup.style.display = "block"; + else + passwordGroup.style.display = "none"; + } + // Determine if IP fields can be edited. + if (ipMode == IP_MODE_DHCP) { + if (!ipField.classList.contains(CLASS_INPUT_DISABLED)) + ipField.classList.add(CLASS_INPUT_DISABLED); + if (!subnetField.classList.contains(CLASS_INPUT_DISABLED)) + subnetField.classList.add(CLASS_INPUT_DISABLED); + if (!gatewayField.classList.contains(CLASS_INPUT_DISABLED)) + gatewayField.classList.add(CLASS_INPUT_DISABLED); + if (!dns1Field.classList.contains(CLASS_INPUT_DISABLED)) + dns1Field.classList.add(CLASS_INPUT_DISABLED); + if (!dns2Field.classList.contains(CLASS_INPUT_DISABLED)) + dns2Field.classList.add(CLASS_INPUT_DISABLED); + } else if (ipMode == IP_MODE_STATIC) { + if (ipField.classList.contains(CLASS_INPUT_DISABLED)) + ipField.classList.remove(CLASS_INPUT_DISABLED); + if (subnetField.classList.contains(CLASS_INPUT_DISABLED)) + subnetField.classList.remove(CLASS_INPUT_DISABLED); + if (gatewayField.classList.contains(CLASS_INPUT_DISABLED)) + gatewayField.classList.remove(CLASS_INPUT_DISABLED); + if (dns1Field.classList.contains(CLASS_INPUT_DISABLED)) + dns1Field.classList.remove(CLASS_INPUT_DISABLED); + if (dns2Field.classList.contains(CLASS_INPUT_DISABLED)) + dns2Field.classList.remove(CLASS_INPUT_DISABLED); + } + // Validate the network interface. + validateInterface(iface); +} + +// Validates the given network interface. +function validateInterface(iface) { + // Initialize vars. + var valid = true; + var saveButton = document.getElementById(iface + FIELD_SAVE_BUTTON); + var enableField = document.getElementById(iface + FIELD_ENABLE); + var ipModeField = document.getElementById(iface + FIELD_IP_MODE); + var ipField = document.getElementById(iface + FIELD_IP_ADDRESS); + var subnetField = document.getElementById(iface + FIELD_SUBNET_MASK); + var gatewayField = document.getElementById(iface + FIELD_DEFAULT_GATEWAY); + var dns1Field = document.getElementById(iface + FIELD_DNS1_ADDRESS); + var dns2Field = document.getElementById(iface + FIELD_DNS2_ADDRESS); + var ipMode = ipModeField.value; + var forceValid = false; + // Validate fields. + if (ipMode == IP_MODE_DHCP) + forceValid = true; + valid &= validateIPField(iface + FIELD_IP_ADDRESS, iface + FIELD_IP_ADDRESS + FIELD_ERROR, forceValid); + valid &= validateIPField(iface + FIELD_SUBNET_MASK, iface + FIELD_SUBNET_MASK + FIELD_ERROR, forceValid); + valid &= validateIPField(iface + FIELD_DEFAULT_GATEWAY, iface + FIELD_DEFAULT_GATEWAY + FIELD_ERROR, forceValid); + valid &= validateIPField(iface + FIELD_DNS1_ADDRESS, iface + FIELD_DNS1_ADDRESS + FIELD_ERROR, forceValid); + valid &= validateIPField(iface + FIELD_DNS2_ADDRESS, iface + FIELD_DNS2_ADDRESS + FIELD_ERROR, forceValid); + if (iface.startsWith(PREFIX_WIFI)) { + valid &= validateField(iface + FIELD_SSID, iface + FIELD_SSID + FIELD_ERROR, REGEX_SSID, ERROR_SSID_INVALID, false); + if (parseInt(document.getElementById(iface + FIELD_ENCRYPTION_TYPE).value) != 0) + valid &= validateField(iface + FIELD_PASSWORD, iface + FIELD_PASSWORD + FIELD_ERROR, REGEX_PASSWORD, ERROR_PASSWORD_INVALID, false); + } + // Check errors. + if (!valid) { + if (!saveButton.classList.contains(CLASS_CONFIG_BUTTON_DISABLED)) + saveButton.classList.add(CLASS_CONFIG_BUTTON_DISABLED); + } else { + if (saveButton.classList.contains(CLASS_CONFIG_BUTTON_DISABLED)) + saveButton.classList.remove(CLASS_CONFIG_BUTTON_DISABLED); + } +} + +// Validates the given IP field. +function validateIPField(fieldID, errorID, forceValid) { + return validateField(fieldID, errorID, REGEX_IP, ERROR_IP_VALUE_FORMAT, forceValid); +} + +// Validates the given field. +function validateField(fieldID, errorID, regexPattern, errorMessage, forceValid) { + // Initialize vars. + var field = document.getElementById(fieldID); + var fieldError = document.getElementById(errorID); + var isValid = true; + var error = ""; + // Sanity checks. + if (field == null || fieldError == null) + return false; + // Check if value is valid. + if (!forceValid) { + var value = field.value; + if (value.length == 0) { + isValid = false; + error = ERROR_FIELD_EMPTY; + } else if (!value.match(regexPattern)) { + isValid = false; + error = errorMessage; + } + } + // Update controls. + if (isValid) { + if (field.classList.contains(CLASS_INPUT_ERROR)) + field.classList.remove(CLASS_INPUT_ERROR); + fieldError.innerHTML = " "; + fieldError.style.display = "none"; + } else { + if (!field.classList.contains(CLASS_INPUT_ERROR)) + field.classList.add(CLASS_INPUT_ERROR); + fieldError.innerHTML = error; + fieldError.style.display = "block"; + } + return isValid; +} + +// Retrieves the given network interface form data. +function getInterfaceData(element, iface) { + // Initialize variables. + var data = {}; + var ifaceData = {}; + var networkData = {}; + var enable = document.getElementById(iface + FIELD_ENABLE).checked; + var ipMode = document.getElementById(iface + FIELD_IP_MODE).value; + // Fill network data. + networkData[ID_ENABLE] = enable; + if (ipMode == IP_MODE_STATIC) { + networkData[ID_IP_MODE] = 0; + networkData[ID_IP] = document.getElementById(iface + FIELD_IP_ADDRESS).value; + networkData[ID_NETMASK] = document.getElementById(iface + FIELD_SUBNET_MASK).value; + networkData[ID_GATEWAY] = document.getElementById(iface + FIELD_DEFAULT_GATEWAY).value; + networkData[ID_DNS1] = document.getElementById(iface + FIELD_DNS1_ADDRESS).value; + networkData[ID_DNS2] = document.getElementById(iface + FIELD_DNS2_ADDRESS).value; + } else { + networkData[ID_IP_MODE] = 1; + } + if (iface.startsWith(PREFIX_WIFI)) { + networkData[ID_SSID] = document.getElementById(iface + FIELD_SSID).value; + networkData[ID_SECURITY_MODE] = parseInt(document.getElementById(iface + FIELD_ENCRYPTION_TYPE).value); + if (networkData[ID_SECURITY_MODE] != 0 && passwordChanged) + networkData[ID_PASSWORD] = document.getElementById(iface + FIELD_PASSWORD).value; + } + ifaceData[iface] = networkData; + data[element] = ifaceData; + return data; +} + +// Saves the network settings for the given interface. +function saveInterface(element, iface) { + // Execute only in the network page. + if (!isNetworkShowing()) + return; + // Hide the info popup. + showInfoPopup(false); + // Show the loading popup. + showLoadingPopup(true, MESSAGE_SAVING_CONFIGURATION); + // Send request to set new configuration. + $.post( + "http://" + getServerAddress() + "/ajax/set_config", + JSON.stringify({ + "configuration": getInterfaceData(element, iface) + }), + function(data) { + // Process only in the network page. + if (!isNetworkShowing()) + return; + // Process answer. + processSaveInterfaceResponse(data, element, iface); + } + ).fail(function(response) { + // Process only in the network page. + if (!isNetworkShowing()) + return; + // Hide the loading panel. + showLoadingPopup(false); + // Process error. + processAjaxErrorResponse(response); + }); +} + +// Processes the save network request response. +function processSaveInterfaceResponse(response, element, iface) { + // Check for error in the response. + if (!checkErrorResponse(response, false)) { + var statusFound = false; + // Check status in the response. + if (response[ID_DATA] != null) { + data = JSON.parse(response[ID_DATA]); + for (const [elementEntry, elementData] of Object.entries(data)) { + if (elementEntry != element) + continue; + for (const [ifaceEntry, ifaceData] of Object.entries(elementData)) { + if (ifaceEntry != iface) + continue; + if (ifaceData[ID_STATUS] != null) { + statusFound = true; + if (ifaceData[ID_STATUS] != 0) + toastr.error(ifaceData[ID_DESC]); + else + toastr.success(MESSAGE_CONFIGURATION_SAVED); + } + } + } + } + if (!statusFound) + toastr.error(ERROR_UNKNOWN); + } + // Hide the loading panel. + showLoadingPopup(false); +} + +// Toggles the visibility of the given interface panel. +function togglePanelVisibility(interface) { + // Initialize variables. + var panelButton = document.getElementById(interface + ID_TOGGLE_BUTTON); + // Sanity checks. + if (panelButton == null) + return; + // Check visibility. + if (panelButton.classList.contains(CLASS_ARROW_UP)) { + $("#" + interface + ID_PANEL_CONTAINER).slideUp("fast", function() { + panelButton.classList.remove(CLASS_ARROW_UP); + panelButton.classList.add(CLASS_ARROW_DOWN); + }); + } else { + $("#" + interface + ID_PANEL_CONTAINER).slideDown("fast", function() { + panelButton.classList.remove(CLASS_ARROW_DOWN); + panelButton.classList.add(CLASS_ARROW_UP); + }); + } +} \ No newline at end of file