diff --git a/Dockerfile b/Dockerfile index c44ff04d..3d8e5307 100755 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ ENV PATH="/opt/venv/bin:$PATH" COPY . ${INSTALL_DIR}/ -RUN pip install graphene flask netifaces tplink-omada-client pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros \ +RUN pip install graphene flask netifaces tplink-omada-client pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros git+https://github.com/foreign-sub/aiofreepybox.git \ && bash -c "find ${INSTALL_DIR} -type d -exec chmod 750 {} \;" \ && bash -c "find ${INSTALL_DIR} -type f -exec chmod 640 {} \;" \ && bash -c "find ${INSTALL_DIR} -type f \( -name '*.sh' -o -name '*.py' -o -name 'speedtest-cli' \) -exec chmod 750 {} \;" diff --git a/front/plugins/freebox/README.md b/front/plugins/freebox/README.md new file mode 100644 index 00000000..4c0aa412 --- /dev/null +++ b/front/plugins/freebox/README.md @@ -0,0 +1,23 @@ +## Overview + +Plugin functionality overview and links to external resources if relevant. Include use cases if available. + +> [!TIP] +> Some tip. + +### Quick setup guide + +To set up the plugin correctly, make sure... + +#### Required Settings + +- When to run `PREF_RUN` +- + +### Usage + +- Head to **Settings** > **Plugin name** to adjust the default values. + +### Notes + +- Additional notes, limitations, Author info. \ No newline at end of file diff --git a/front/plugins/freebox/config.json b/front/plugins/freebox/config.json new file mode 100644 index 00000000..f04cae74 --- /dev/null +++ b/front/plugins/freebox/config.json @@ -0,0 +1,504 @@ +{ + "code_name": "freebox", + "unique_prefix": "FREEBOX", + "plugin_type": "device_scanner", + "execution_order": "Layer_0", + "enabled": true, + "data_source": "script", + "mapped_to_table": "CurrentScan", + "data_filters": [ + { + "compare_column": "Object_PrimaryID", + "compare_operator": "==", + "compare_field_id": "txtMacFilter", + "compare_js_template": "'{value}'.toString()", + "compare_use_quotes": true + } + ], + "show_ui": true, + "localized": [ + "display_name", + "description", + "icon" + ], + "display_name": [ + { + "language_code": "en_us", + "string": "Freebox" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Plugin to discover devices and names via Freebox routers" + } + ], + "icon": [ + { + "language_code": "en_us", + "string": "" + } + ], + "params": [], + "settings": [ + { + "function": "RUN", + "events": [ + "run" + ], + "type": { + "dataType": "string", + "elements": [ + { + "elementType": "select", + "elementOptions": [], + "transformers": [] + } + ] + }, + "default_value": "disabled", + "options": [ + "disabled", + "once", + "schedule", + "always_after_scan", + "on_new_device", + "on_notification" + ], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "When to run" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "When the plugin should run." + } + ] + }, + { + "function": "RUN_SCHD", + "type": { + "dataType": "string", + "elements": [ + { + "elementType": "input", + "elementOptions": [], + "transformers": [] + } + ] + }, + "default_value": "*/5 * * * *", + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "Schedule" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Only enabled if you select schedule in the SYNC_RUN setting. Make sure you enter the schedule in the correct cron-like format (e.g. validate at crontab.guru). For example entering 0 4 * * * will run the scan after 4 am in the TIMEZONE you set above. Will be run NEXT time the time passes." + } + ] + }, + { + "function": "address", + "type": { + "dataType": "string", + "elements": [ + { + "elementType": "input", + "elementOptions": [], + "transformers": [] + } + ] + }, + "maxLength": 50, + "default_value": "mafreebox.freebox.fr", + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "Address" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Address of the Freebox router. You will need to pair the device as explained in the docs" + } + ] + }, + { + "function": "api_version", + "type": { + "dataType": "integer", + "elements": [ + { + "elementType": "input", + "elementOptions": [ + { + "type": "number" + } + ], + "transformers": [] + } + ] + }, + "default_value": 6, + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "API version" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Which version of the API o use" + } + ] + }, + { + "function": "api_port", + "type": { + "dataType": "integer", + "elements": [ + { + "elementType": "input", + "elementOptions": [ + { + "type": "number" + } + ], + "transformers": [] + } + ] + }, + "default_value": 443, + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "HTTP(S) port" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Port for remote HTTP(S) access. This will be different for your device." + } + ] + }, + { + "function": "CMD", + "type": { + "dataType": "string", + "elements": [ + { + "elementType": "input", + "elementOptions": [ + { + "readonly": "" + } + ], + "transformers": [] + } + ] + }, + "default_value": "python3 /app/front/plugins/freebox/freebox.py", + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "Command" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Command to run. This can not be changed" + } + ] + }, + { + "function": "RUN_TIMEOUT", + "type": { + "dataType": "integer", + "elements": [ + { + "elementType": "input", + "elementOptions": [ + { + "type": "number" + } + ], + "transformers": [] + } + ] + }, + "default_value": 30, + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "Run timeout" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted." + } + ] + } + ], + "database_column_definitions": [ + { + "column": "Index", + "css_classes": "col-sm-2", + "show": true, + "type": "none", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Index" + } + ] + }, + { + "column": "Object_PrimaryID", + "mapped_to_column": "cur_MAC", + "css_classes": "col-sm-2", + "show": true, + "type": "device_name_mac", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "MAC" + } + ] + }, + { + "column": "Object_SecondaryID", + "mapped_to_column": "cur_IP", + "css_classes": "col-sm-2", + "show": true, + "type": "device_ip", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "IP" + } + ] + }, + { + "column": "Watched_Value1", + "mapped_to_column": "cur_Name", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Name" + } + ] + }, + { + "column": "Watched_Value2", + "mapped_to_column": "cur_Vendor", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Vendor" + } + ] + }, + { + "column": "Watched_Value3", + "mapped_to_column": "cur_Type", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Device Type" + } + ] + }, + { + "column": "Watched_Value4", + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "N/A" + } + ] + }, + { + "column": "Dummy", + "mapped_to_column": "cur_ScanMethod", + "mapped_to_column_data": { + "value": "Freebox" + }, + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Scan method" + } + ] + }, + { + "column": "DateTimeCreated", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Created" + } + ] + }, + { + "column": "DateTimeChanged", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Changed" + } + ] + }, + { + "column": "Status", + "css_classes": "col-sm-1", + "show": true, + "type": "replace", + "default_value": "", + "options": [ + { + "equals": "watched-not-changed", + "replacement": "
" + }, + { + "equals": "watched-changed", + "replacement": "
" + }, + { + "equals": "new", + "replacement": "
" + }, + { + "equals": "missing-in-last-scan", + "replacement": "
" + } + ], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Status" + } + ] + } + ] +} \ No newline at end of file diff --git a/front/plugins/freebox/freebox.py b/front/plugins/freebox/freebox.py new file mode 100644 index 00000000..593345be --- /dev/null +++ b/front/plugins/freebox/freebox.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python + +import os +import pathlib +import sys +import json +import sqlite3 +from pytz import timezone +import asyncio +from datetime import datetime +from pathlib import Path +from typing import cast +import socket +import aiofreepybox +from aiofreepybox import Freepybox +from aiofreepybox.api.lan import Lan +from aiofreepybox.exceptions import NotOpenError, AuthorizationError + +# Define the installation path and extend the system path for plugin imports +INSTALL_PATH = "/app" +sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) + +from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64 +from plugin_utils import get_plugins_configs +from logger import mylog +from const import pluginsPath, fullDbPath +from helper import timeNowTZ, get_setting_value +from notification import write_notification +import conf + +# Make sure the TIMEZONE for logging is correct +conf.tz = timezone(get_setting_value("TIMEZONE")) + +# Define the current path and log file paths +CUR_PATH = str(Path(__file__).parent.resolve()) +LOG_FILE = os.path.join(CUR_PATH, "script.log") +RESULT_FILE = os.path.join(CUR_PATH, "last_result.log") + +# Initialize the Plugin obj output file +plugin_objects = Plugin_Objects(RESULT_FILE) + +pluginName = "FREEBOX" + +device_type_map = { + "workstation": "PC", + "laptop": "Laptop", + "smartphone": "Smartphone", + "tablet": "Tablet", + "printer": "Printer", + "vg_console": "Game Console", + "television": "SmartTV", + "nas": "NAS", + "ip_camera": "IP Camera", + "ip_phone": "Phone", + "freebox_player": "TV Decoder", + "freebox_hd": "TV Decoder", + "freebox_crystal": "TV Decoder", + "freebox_mini": "TV Decoder", + "freebox_delta": "Gateway", + "freebox_one": "Gateway", + "freebox_wifi": "Gateway", + "freebox_pop": "AP", + "networking_device": "Router", + "multimedia_device": "TV Decoder", + "car": "House Appliance", + "other": "(Unknown)", +} + + +def map_device_type(type: str): + return device_type_map[type] + + +async def get_device_data(api_version: int, api_address: str, api_port: int): + # ensure existence of db path + data_dir = Path("/app/config/freeboxdb") + data_dir.mkdir(parents=True, exist_ok=True) + + # Instantiate Freepybox class using default application descriptor + # and custom token_file location + fbx = Freepybox( + app_desc={ + "app_id": "netalertx", + "app_name": "NetAlertX", + "app_version": aiofreepybox.__version__, + "device_name": socket.gethostname(), + }, + api_version="v" + str(api_version), + data_dir=data_dir, + ) + + # Connect to the freebox + # Be ready to authorize the application on the Freebox if you run this + # for the first time + try: + await fbx.open(host=api_address, port=api_port) + except NotOpenError as e: + mylog("verbose", [f"[{pluginName}] Error connecting to freebox: {e}"]) + except AuthorizationError as e: + mylog("verbose", [f"[{pluginName}] Auth error: {str(e)}"]) + + # get also info of the freebox itself + config = await fbx.system.get_config() + freebox = await cast(Lan, fbx.lan).get_config() + hosts = await cast(Lan, fbx.lan).get_hosts_list() + assert config is not None + assert freebox is not None + freebox["mac"] = config["mac"] + freebox["operator"] = config["model_info"]["net_operator"] + + # Close the freebox session + await fbx.close() + + return freebox, hosts + + +def main(): + mylog("verbose", [f"[{pluginName}] In script"]) + + # Retrieve configuration settings + api_settings = { + "api_address": get_setting_value("FREEBOX_address"), + "api_version": get_setting_value("FREEBOX_api_version"), + "api_port": get_setting_value("FREEBOX_api_port"), + } + + mylog("verbose", [f"[{pluginName}] Settings: {api_settings}"]) + + # retrieve data + loop = asyncio.new_event_loop() + freebox, hosts = loop.run_until_complete(get_device_data(**api_settings)) + loop.close() + + mylog("verbose", [freebox]) + mylog("verbose", [hosts]) + + plugin_objects.add_object( + primaryId=freebox["mac"], + secondaryId=freebox["ip"], + watched1=freebox["name"], + watched2=freebox["operator"], + watched3="Gateway", + watched4=datetime.now, + extra="", + foreignKey=freebox["mac"], + ) + for host in hosts: + for ip in [ip for ip in host["l3connectivities"] if ip["reachable"]]: + mac: str = host["l2ident"]["id"] + plugin_objects.add_object( + primaryId=mac, + secondaryId=ip["addr"], + watched1=host["primary_name"], + watched2=host["vendor_name"] if host["vendor_name"] else "(unknown)", + watched3=map_device_type(host["host_type"]), + watched4=datetime.fromtimestamp(ip["last_time_reachable"]), + extra="", + foreignKey=mac, + ) + + # commit result + plugin_objects.write_result_file() + + return 0 + + +if __name__ == "__main__": + main()