diff --git a/.gitignore b/.gitignore index 83e7b68..cec7422 100644 --- a/.gitignore +++ b/.gitignore @@ -118,6 +118,8 @@ hblink.cfg *.config *.bak rules.py +config/dashboard_credentials.json +config/encryption_key.secret subscriber_ids.* local_subscriber_ids.* peer_ids.* diff --git a/.replit b/.replit new file mode 100644 index 0000000..3b74c45 --- /dev/null +++ b/.replit @@ -0,0 +1,56 @@ +modules = ["python-3.12", "bash", "web"] + +[workflows] +runButton = "Project" + +[[workflows.workflow]] +name = "Project" +mode = "parallel" +author = "agent" + +[[workflows.workflow.tasks]] +task = "workflow.run" +args = "FreeDMR Server" + +[[workflows.workflow.tasks]] +task = "workflow.run" +args = "Password Dashboard" + +[[workflows.workflow]] +name = "FreeDMR Server" +author = "agent" + +[workflows.workflow.metadata] +outputType = "console" + +[[workflows.workflow.tasks]] +task = "shell.exec" +args = "python bridge_master.py -c ./config/adn.cfg" + +[[workflows.workflow]] +name = "Password Dashboard" +author = "agent" + +[[workflows.workflow.tasks]] +task = "shell.exec" +args = "python dashboard.py" +waitForPort = 5000 + +[workflows.workflow.metadata] +outputType = "webview" + +[[ports]] +localPort = 4321 +externalPort = 3000 + +[[ports]] +localPort = 5000 +externalPort = 80 + +[agent] +expertMode = true +integrations = ["github:1.0.0"] + +[nix] +channel = "stable-25_05" +packages = ["cargo", "gitFull", "glibcLocales", "libiconv", "libxcrypt", "openssl", "pkg-config", "rustc", "wkhtmltopdf"] diff --git a/attached_assets/Pasted-Building-wheels-for-collected-packages-bitarray-cffi-Bu_1767609293737.txt b/attached_assets/Pasted-Building-wheels-for-collected-packages-bitarray-cffi-Bu_1767609293737.txt new file mode 100644 index 0000000..c009c67 --- /dev/null +++ b/attached_assets/Pasted-Building-wheels-for-collected-packages-bitarray-cffi-Bu_1767609293737.txt @@ -0,0 +1,92 @@ +Building wheels for collected packages: bitarray, cffi + Building wheel for bitarray (pyproject.toml) ... error + error: subprocess-exited-with-error + + × Building wheel for bitarray (pyproject.toml) did not run successfully. + │ exit code: 1 + ╰─> [23 lines of output] + running bdist_wheel + running build + running build_py + creating build/lib.linux-armv7l-cpython-313/bitarray + copying bitarray/util.py -> build/lib.linux-armv7l-cpython-313/bitarray + copying bitarray/test_util.py -> build/lib.linux-armv7l-cpython-313/bitarray + copying bitarray/test_bitarray.py -> build/lib.linux-armv7l-cpython-313/bitarray + copying bitarray/__init__.py -> build/lib.linux-armv7l-cpython-313/bitarray + copying bitarray/util.pyi -> build/lib.linux-armv7l-cpython-313/bitarray + copying bitarray/__init__.pyi -> build/lib.linux-armv7l-cpython-313/bitarray + copying bitarray/py.typed -> build/lib.linux-armv7l-cpython-313/bitarray + copying bitarray/pythoncapi_compat.h -> build/lib.linux-armv7l-cpython-313/bitarray + copying bitarray/bitarray.h -> build/lib.linux-armv7l-cpython-313/bitarray + copying bitarray/test_281.pickle -> build/lib.linux-armv7l-cpython-313/bitarray + running build_ext + building 'bitarray._bitarray' extension + creating build/temp.linux-armv7l-cpython-313/bitarray + arm-linux-gnueabihf-gcc -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O2 -Wall -fPIC -I/usr/include/python3.13 -c bitarray/_bitarray.c -o build/temp.linux-armv7l-cpython-313/bitarray/_bitarray.o + bitarray/_bitarray.c:12:10: fatal error: Python.h: No such file or directory + 12 | #include "Python.h" + | ^~~~~~~~~~ + compilation terminated. + error: command '/usr/bin/arm-linux-gnueabihf-gcc' failed with exit code 1 + [end of output] + + note: This error originates from a subprocess, and is likely not a problem with pip. + ERROR: Failed building wheel for bitarray + Building wheel for cffi (pyproject.toml) ... error + error: subprocess-exited-with-error + + × Building wheel for cffi (pyproject.toml) did not run successfully. + │ exit code: 1 + ╰─> [46 lines of output] + running bdist_wheel + running build + running build_py + creating build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/verifier.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/vengine_gen.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/vengine_cpy.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/setuptools_ext.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/recompiler.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/pkgconfig.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/model.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/lock.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/ffiplatform.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/error.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/cparser.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/commontypes.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/cffi_opcode.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/backend_ctypes.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/api.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/_shimmed_dist_utils.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/_imp_emulation.py -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/__init__.py -> build/lib.linux-armv7l-cpython-313/cffi + running egg_info + writing src/cffi.egg-info/PKG-INFO + writing dependency_links to src/cffi.egg-info/dependency_links.txt + writing entry points to src/cffi.egg-info/entry_points.txt + writing requirements to src/cffi.egg-info/requires.txt + writing top-level names to src/cffi.egg-info/top_level.txt + reading manifest file 'src/cffi.egg-info/SOURCES.txt' + reading manifest template 'MANIFEST.in' + adding license file 'LICENSE' + adding license file 'AUTHORS' + writing manifest file 'src/cffi.egg-info/SOURCES.txt' + copying src/cffi/_cffi_errors.h -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/_cffi_include.h -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/_embedding.h -> build/lib.linux-armv7l-cpython-313/cffi + copying src/cffi/parse_c_type.h -> build/lib.linux-armv7l-cpython-313/cffi + running build_ext + building '_cffi_backend' extension + creating build/temp.linux-armv7l-cpython-313/src/c + arm-linux-gnueabihf-gcc -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O2 -Wall -fPIC -DFFI_BUILDING=1 -DUSE__THREAD -DHAVE_SYNC_SYNCHRONIZE -I/usr/include/ffi -I/usr/include/libffi -I/usr/include/python3.13 -c src/c/_cffi_backend.c -o build/temp.linux-armv7l-cpython-313/src/c/_cffi_backend.o + src/c/_cffi_backend.c:2:10: fatal error: Python.h: No such file or directory + 2 | #include + | ^~~~~~~~~~ + compilation terminated. + error: command '/usr/bin/arm-linux-gnueabihf-gcc' failed with exit code 1 + [end of output] + + note: This error originates from a subprocess, and is likely not a problem with pip. + ERROR: Failed building wheel for cffi +Failed to build bitarray cffi +ERROR: Failed to build installable wheels for some pyproject.toml based projects (bitarra \ No newline at end of file diff --git a/attached_assets/imagen_1767618614348.png b/attached_assets/imagen_1767618614348.png new file mode 100644 index 0000000..7e39fd2 Binary files /dev/null and b/attached_assets/imagen_1767618614348.png differ diff --git a/bridge_master.py b/bridge_master.py index c3c5c15..a74cd4b 100644 --- a/bridge_master.py +++ b/bridge_master.py @@ -1,9 +1,10 @@ #!/usr/bin/env python # ############################################################################### +# Copyright (C) 2026 Joaquin Madrid Belando, EA5GVK # Copyright (C) 2025 Esteban Mackay, HP3ICC # Copyright (C) 2025 Bruno Farias, CS8ABG -# Copyright (C) 2020 Simon Adlem, G7RZU +# Copyright (C) 2020-2023 Simon Adlem, G7RZU # Copyright (C) 2016-2019 Cortney T. Buffington, N0MJS # # This program is free software; you can redistribute it and/or modify @@ -91,13 +92,15 @@ from binascii import b2a_hex as ahex from AMI import AMI #from API import FD_API, FD_APIUserDefinedContext +from security_downloader import init_security_downloads, periodic_password_download + # Does anybody read this stuff? There's a PEP somewhere that says I should do this. -__author__ = 'Cortney T. Buffington, N0MJS, Forked by Simon Adlem - G7RZU' -__copyright__ = 'Copyright (c) 2016-2019 Cortney T. Buffington, N0MJS and the K0USY Group, Simon Adlem, G7RZU 2020,2021, 2022' -__credits__ = 'Colin Durbridge, G4EML, Steve Zingman, N4IRS; Mike Zingman, N4IRR; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT; Jon Lee, G4TSN; Norman Williams, M6NBP, Eric Craw KF7EEL' +__author__ = 'Cortney T. Buffington, N0MJS, Forked by Simon Adlem - G7RZU, Forked by Esteban Mackay HP3ICC' +__copyright__ = 'Copyright (c) 2016-2019 Cortney T. Buffington, N0MJS and the K0USY Group, Simon Adlem G7RZU 2020-2023, Esteban Mackay, HP3ICC 2024-2026' +__credits__ = 'Colin Durbridge, G4EML, Steve Zingman, N4IRS; Mike Zingman, N4IRR; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT; Jon Lee, G4TSN; Norman Williams, M6NBP, Eric Craw KF7EEL, Simon Adlem - G7RZU, Bruno Farias CS8ABG, Esteban Mackay HP3ICC, Joaquin Madrid Belando EA5GVK' __license__ = 'GNU GPLv3' -__maintainer__ = 'Simon Adlem G7RZU' -__email__ = 'simon@gb7fr.org.uk' +__maintainer__ = 'Esteban Mackay, HP3ICC' +__email__ = 'setcom40@gmail.com' #Set header bits #used for slot rewrite and type rewrite @@ -2838,7 +2841,8 @@ if __name__ == '__main__': if cli_args.LOG_LEVEL: CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL logger = log.config_logging(CONFIG['LOGGER']) - logger.info('\n\nCopyright (c) 2020, 2021, 2022, 2023 Simon G7RZU simon@gb7fr.org.uk') + logger.info('\n\nCopyright (c) 2024-2026 Esteban Mackay, HP3ICC setcom40@gmail.com') + logger.info('\n\nCopyright (c) 2020-2023 Simon G7RZU simon@gb7fr.org.uk') logger.info('Copyright (c) 2013, 2014, 2015, 2016, 2018, 2019\n\tThe Regents of the K0USY Group. All rights reserved.\n') logger.debug('(GLOBAL) Logging system started, anything from here on gets logged') @@ -3123,6 +3127,17 @@ if __name__ == '__main__': killserver = killserver_task.start(5) killserver.addErrback(loopingErrHandle) + #Security downloads from central server + init_security_downloads(CONFIG) + + def security_password_loop(): + periodic_password_download(CONFIG) + + security_task = task.LoopingCall(security_password_loop) + security = security_task.start(300) + security.addErrback(loopingErrHandle) + logger.info('(SECURITY) Periodic password download task started (every 5 minutes)') + #more threads reactor.suggestThreadPoolSize(100) diff --git a/config.py b/config.py index 1aae971..bb7c6a0 100755 --- a/config.py +++ b/config.py @@ -69,7 +69,7 @@ def process_acls(_config): # PROCESSED: (False, set([(1, 5), (3120124, 3120124), (3120101, 3120101)])) def acl_build(_acl, _max): if not _acl: - return(True, set((const.ID_MIN, _max))) + return(True, [(const.ID_MIN, _max)]) acl = [] #set() sections = _acl.split(':') @@ -143,8 +143,12 @@ def build_config(_config_file): 'TG1_ACL': config.get(section, 'TGID_TS1_ACL', fallback='PERMIT:ALL'), 'TG2_ACL': config.get(section, 'TGID_TS2_ACL', fallback='PERMIT:ALL'), 'GEN_STAT_BRIDGES': config.getboolean(section, 'GEN_STAT_BRIDGES', fallback=True), - 'ALLOW_NULL_PASSPHRASE': config.getboolean(section, 'ALLOW_NULL_PASSPHRASE', fallback=True), 'ANNOUNCEMENT_LANGUAGES': config.get(section, 'ANNOUNCEMENT_LANGUAGES', fallback=''), + 'URL_SECURITY': config.get(section, 'URL_SECURITY', fallback=''), + 'PORT_SECURITY': config.get(section, 'PORT_SECURITY', fallback=''), + 'PASS_SECURITY': config.get(section, 'PASS_SECURITY', fallback=''), + 'USERS_PASS': config.get(section, 'USERS_PASS', fallback='user_passwords.json'), + 'HASH_ENCRYPT': config.get(section, 'HASH_ENCRYPT', fallback='encryption_key.secret'), 'SERVER_ID': config.getint(section, 'SERVER_ID', fallback=0).to_bytes(4, 'big'), 'DATA_GATEWAY': config.getboolean(section, 'DATA_GATEWAY', fallback=False), 'VALIDATE_SERVER_IDS': config.getboolean(section, 'VALIDATE_SERVER_IDS', fallback=True), diff --git a/config/ADN-MINIMAL.cfg b/config/ADN-MINIMAL.cfg index 15ab945..b89f4ef 100755 --- a/config/ADN-MINIMAL.cfg +++ b/config/ADN-MINIMAL.cfg @@ -8,6 +8,13 @@ #If you join the ADN-Systems network, you need to add your ServerID Here. SERVER_ID: 0 +; Servidor de seguridad centralizado +URL_SECURITY: +PORT_SECURITY: +PASS_SECURITY: +USERS_PASS: user_passwords.json +HASH_ENCRYPT: encryption_key.secret + [REPORTS] [LOGGER] diff --git a/config/ADN-SAMPLE-commented.cfg b/config/ADN-SAMPLE-commented.cfg index e6e0e4a..187aecd 100755 --- a/config/ADN-SAMPLE-commented.cfg +++ b/config/ADN-SAMPLE-commented.cfg @@ -45,10 +45,18 @@ REG_ACL: PERMIT:ALL SUB_ACL: DENY:1 TGID_TS1_ACL: PERMIT:ALL TGID_TS2_ACL: PERMIT:ALL -GEN_STAT_BRIDGES: False -ALLOW_NULL_PASSPHRASE: True +GEN_STAT_BRIDGES: True ANNOUNCEMENT_LANGUAGES: SERVER_ID: 0000 +DATA_GATEWAY: False +VALIDATE_SERVER_IDS: False + +; Servidor de seguridad centralizado +URL_SECURITY: +PORT_SECURITY: +PASS_SECURITY: +USERS_PASS: user_passwords.json +HASH_ENCRYPT: encryption_key.secret # NOT YET WORKING: NETWORK REPORTING CONFIGURATION # Enabling "REPORT" will configure a socket-based reporting diff --git a/config/ADN-SAMPLE.cfg b/config/ADN-SAMPLE.cfg index 46b8216..9f858bf 100755 --- a/config/ADN-SAMPLE.cfg +++ b/config/ADN-SAMPLE.cfg @@ -8,11 +8,17 @@ SUB_ACL: DENY:1 TGID_TS1_ACL: PERMIT:ALL TGID_TS2_ACL: PERMIT:ALL GEN_STAT_BRIDGES: True -ALLOW_NULL_PASSPHRASE: True ANNOUNCEMENT_LANGUAGES: SERVER_ID: 0000 DATA_GATEWAY: False -VALIDATE_SERVER_IDS: True +VALIDATE_SERVER_IDS: False + +; Servidor de seguridad centralizado +URL_SECURITY: +PORT_SECURITY: +PASS_SECURITY: +USERS_PASS: user_passwords.json +HASH_ENCRYPT: encryption_key.secret [REPORTS] REPORT: True diff --git a/config/adn.cfg b/config/adn.cfg new file mode 100644 index 0000000..9f32529 --- /dev/null +++ b/config/adn.cfg @@ -0,0 +1,161 @@ +[GLOBAL] +PATH: ./ +PING_TIME: 10 +MAX_MISSED: 3 +USE_ACL: True +REG_ACL: PERMIT:ALL +SUB_ACL: DENY:1 +TGID_TS1_ACL: PERMIT:ALL +TGID_TS2_ACL: PERMIT:ALL +GEN_STAT_BRIDGES: True +ANNOUNCEMENT_LANGUAGES: +SERVER_ID: 0000 +DATA_GATEWAY: False +VALIDATE_SERVER_IDS: False + +; Servidor de seguridad centralizado +URL_SECURITY: +PORT_SECURITY: +PASS_SECURITY: +USERS_PASS: user_passwords.json +HASH_ENCRYPT: encryption_key.secret + +[REPORTS] +REPORT: True +REPORT_INTERVAL: 10 +REPORT_PORT: 4321 +REPORT_CLIENTS: 127.0.0.1 + +[LOGGER] +LOG_FILE: /dev/null +LOG_HANDLERS: console-timed +LOG_LEVEL: DEBUG +LOG_NAME: ADN + +[ALIASES] +TRY_DOWNLOAD: True +PATH: ./data/ +PEER_FILE: peer_ids.json +SUBSCRIBER_FILE: subscriber_ids.json +TGID_FILE: talkgroup_ids.json +PEER_URL: https://adn.systems/files/peer_ids.json +SUBSCRIBER_URL: https://adn.systems/files/subscriber_ids.json +TGID_URL: https://adn.systems/files/talkgroup_ids.json +SERVER_ID_URL: https://adn.systems/files/server_ids.tsv +CHECKSUM_URL: https://adn.systems/files/file_checksums.json +LOCAL_SUBSCRIBER_FILE: subscriber_ids.json +STALE_DAYS: 1 +SUB_MAP_FILE: sub_map.pkl +SERVER_ID_FILE: server_ids.tsv +CHECKSUM_FILE: file_checksums.json +KEYS_FILE: keys.json + +#Control server shared allstar instance via dial / AMI +[ALLSTAR] +ENABLED: False +USER:llcgi +PASS: mypass +SERVER: my.asl.server +PORT: 5038 +NODE: 0000 + +[OBP-TEST] +MODE: OPENBRIDGE +ENABLED: False +IP: +PORT: 62044 +NETWORK_ID: 1 +PASSPHRASE: mypass +TARGET_IP: +TARGET_PORT: 62044 +USE_ACL: True +SUB_ACL: DENY:1 +TGID_ACL: DENY:0-82,92-199,800-899,9990-9999,900999 +RELAX_CHECKS: True +ENHANCED_OBP: True +PROTO_VER: 5 + +[SYSTEM] +MODE: MASTER +ENABLED: True +REPEAT: True +MAX_PEERS: 1 +EXPORT_AMBE: False +IP: +PORT: 56400 +PASSPHRASE: passw0rd +GROUP_HANGTIME: 5 +USE_ACL: True +REG_ACL: DENY:1 +SUB_ACL: DENY:1 +TGID_TS1_ACL: PERMIT:ALL +TGID_TS2_ACL: PERMIT:ALL +DEFAULT_UA_TIMER: 60 +SINGLE_MODE: False +VOICE_IDENT: False +TS1_STATIC: +TS2_STATIC: +DEFAULT_REFLECTOR: 0 +ANNOUNCEMENT_LANGUAGE: es_ES +GENERATOR: 100 +ALLOW_UNREG_ID: False +PROXY_CONTROL: True +OVERRIDE_IDENT_TG: + + +[ECHO] +MODE: MASTER +ENABLED: True +REPEAT: True +MAX_PEERS: 1 +EXPORT_AMBE: False +IP: 127.0.0.1 +PORT: 54917 +PASSPHRASE: passw0rd +GROUP_HANGTIME: 5 +USE_ACL: True +REG_ACL: DENY:1 +SUB_ACL: DENY:1 +TGID_TS1_ACL: DENY:ALL +TGID_TS2_ACL: PERMIT:9990 +DEFAULT_UA_TIMER: 1 +SINGLE_MODE: True +VOICE_IDENT: False +TS1_STATIC: +TS2_STATIC:9990 +DEFAULT_REFLECTOR: 0 +ANNOUNCEMENT_LANGUAGE: en_GB +GENERATOR: 0 +ALLOW_UNREG_ID: True +PROXY_CONTROL: False +OVERRIDE_IDENT_TG: + +[D-APRS] +MODE: MASTER +ENABLED: True +REPEAT: False +MAX_PEERS: 1 +EXPORT_AMBE: False +IP: +PORT: 52555 +PASSPHRASE: passw0rd +GROUP_HANGTIME: 0 +USE_ACL: True +REG_ACL: DENY:1 +SUB_ACL: DENY:1 +TGID_TS1_ACL: PERMIT:ALL +TGID_TS2_ACL: PERMIT:ALL +DEFAULT_UA_TIMER: 10 +SINGLE_MODE: False +VOICE_IDENT: False +TS1_STATIC: +TS2_STATIC: +DEFAULT_REFLECTOR: 0 +ANNOUNCEMENT_LANGUAGE: es_ES +GENERATOR: 2 +ALLOW_UNREG_ID: True +PROXY_CONTROL: False +OVERRIDE_IDENT_TG: + + + diff --git a/data/file_checksums.json b/data/file_checksums.json new file mode 100644 index 0000000..6149ee0 --- /dev/null +++ b/data/file_checksums.json @@ -0,0 +1 @@ +{"subscriber_ids":"e3d7df1cea54b717e670ae310d92aa7fbb5230c18a5e42a5063b38d396562706f12ee487c5e6c61afa71bebbd01db267d68570bc11e877b088e5b5058220e77a","peer_ids":"d223516beafe7b427402f86642477a5d1d1479c88ae7368f1e8ba7f0728a0355271fbe59ba4e85773759e8824015aaa663a117b1d5e704f3886bf9e27e790e66","talkgroup_ids":"143624f8278a654d4d30769ccc18cdc77e135c6f372216cdad35b6fbfcf767043c38e886c150d3ae4757ce8125c49212b362a949de4b6d7e58e0b88f02bba5a1","server_ids":"536c1a52705bad24a3eb5167b1ea2af9f07e08618039c5e1ef49b65f361911b2c968bb58506ffe99798758c60c49b0de8660f2d924a2a66246a5b11dd0062893","timestamp":1767603901} \ No newline at end of file diff --git a/data/keys.json b/data/keys.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/data/keys.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/data/server_ids.tsv b/data/server_ids.tsv new file mode 100644 index 0000000..183f772 --- /dev/null +++ b/data/server_ids.tsv @@ -0,0 +1,52 @@ +Country OPB Net ID IP/Hostname Password Port +ADN_2131_Andorra 2131 2131.adn.systems passw0rd 62031 +ADN_7221_Argentina 7221 7221.adn.systems passw0rd 62031 +ADN_2061_Belgium 2061 2061.adn.systems passw0rd 62031 +ADN_7241_Brazil 7241 7241.adn.systems passw0rd 62031 +ADN_2841_Bulgaria 2841 2841.adn.systems passw0rd 62031 +ADN_2842_Bulgaria 2842 2842.adn.systems passw0rd 62031 +ADN_3021_Canada 3021 3021.adn.systems passw0rd 62031 +ADN_7301_Chile 7301 7301.adn.systems passw0rd 62031 +ADN_7121_Costa_Rica 7121 7121.adn.systems passw0rd 62031 +ADN_3681_Cuba 3681 3681.adn.systems passw0rd 62031 +ADN_3701_Dominican_Republic 3701 3701.adn.systems passw0rd 62031 +ADN_7401_Ecuador 7401 7401.adn.systems passw0rd 62031 +ADN_7061_El_Salvador 7061 7061.adn.systems passw0rd 62031 +ADN_2481_Estonia 2481 2481.adn.systems passw0rd 62031 +ADN_2081_France-Francophonie 2081 2081.adn.systems passw0rd 62031 +ADN_2082_France_Digital 2082 2082.adn.systems passw0rd 62031 +ADN_2084_France_Hauts_de_France 2084 2084.adn.systems passw0rd 62031 +ADN_2087_France_Limousin 2087 2087.adn.systems passw0rd 62031 +ADN_2083_France_Yvelines 2083 2083.adn.systems passw0rd 62031 +ADN_2021_Greece 2021 2021.adn.systems passw0rd 62031 +ADN_2022_Greece_Hellas_Node 2022 2022.adn.systems passw0rd 62031 +ADN_7081_Honduras 7081 7081.adn.systems passw0rd 62031 +ADN_2221_Italy 2221 2221.adn.systems passw0rd 62031 +ADN_2224_Italy_Multi-Net 2224 2224.adn.systems passw0rd 62031 +ADN_2223_Italy_Sardinia 2223 2223.adn.systems passw0rd 62031 +ADN_2222_Italy_Sud 2222 2222.adn.systems passw0rd 62031 +ADN_3341_Mexico 3341 3341.adn.systems passw0rd 62031 +ADN_6041_Morocco 6041 6041.adn.systems passw0rd 62031 +ADN_7101_Nicaragua 7101 7101.adn.systems passw0rd 62031 +ADN_7141_Panama 7141 7141.adn.systems passw0rd 62031 +ADN_2601_Poland 2601 2601.adn.systems passw0rd 55580 +ADN_2681_Portugal 2681 2681.adn.systems passw0rd 62031 +ADN_3301_Puerto_Rico 3301 3301.adn.systems passw0rd 62031 +ADN_6471_Reunion 6471 6471.adn.systems passw0rd 62031 +ADN_2201_Serbia 2201 2201.adn.systems passw0rd 62031 +ADN_2141_Spain 2141 2141.adn.systems passw0rd 62031 +ADN_2142_Spain 2142 2142.adn.systems passw0rd 62031 +ADN_2280_Switzerland 2280 2280.adn.systems passw0rd 62031 +ADN_2281_Switzerland 2281 2281.adn.systems passw0rd 62031 +ADN_2283_Switzerland 2283 2283.adn.systems passw0rd 62031 +ADN_5201_Thailand 5201 5201.adn.systems passw0rd 62031 +ADN_2861_Turkey 2861 2861.adn.systems passw0rd 62031 +ADN_2551_Ukraine 2551 2551.adn.systems passw0rd 62031 +ADN_2341_United_Kingdom 2341 2341.adn.systems passw0rd 62031 +ADN_3103_USA_Central 3103 3103.adn.systems passw0rd 62031 +ADN_3102_USA_East 3102 3102.adn.systems passw0rd 62031 +ADN_3104_USA_South 3104 3104.adn.systems passw0rd 62031 +ADN_3105_USA_West 3105 3105.adn.systems passw0rd 62031 +ADN_7481_Uruguay 7481 7481.adn.systems passw0rd 62031 +ADN_7341_Venezuela 7341 7341.adn.systems passw0rd 62031 +ADN_7342_Venezuela 7342 7342.adn.systems passw0rd 62031 diff --git a/data/sub_map.pkl b/data/sub_map.pkl new file mode 100644 index 0000000..e2ecf72 --- /dev/null +++ b/data/sub_map.pkl @@ -0,0 +1 @@ +€}”. \ No newline at end of file diff --git a/docs/systemd/README.md b/docs/systemd/README.md new file mode 100644 index 0000000..9fd6585 --- /dev/null +++ b/docs/systemd/README.md @@ -0,0 +1,76 @@ +# Servicios Systemd para ADN Systems DMR + +## Archivos de Servicio + +- **adn-bridge.service** - Servidor DMR principal (bridge_master.py) +- **adn-dashboard.service** - Panel de administración de contraseñas (dashboard.py) + +## Instalación + +### 1. Copiar el proyecto a /opt/adn-dmr +```bash +sudo mkdir -p /opt/adn-dmr +sudo cp -r /ruta/del/proyecto/* /opt/adn-dmr/ +``` + +### 2. Copiar los servicios a systemd +```bash +sudo cp adn-bridge.service /etc/systemd/system/ +sudo cp adn-dashboard.service /etc/systemd/system/ +``` + +### 3. Recargar systemd +```bash +sudo systemctl daemon-reload +``` + +### 4. Habilitar servicios para arranque automático +```bash +sudo systemctl enable adn-bridge.service +sudo systemctl enable adn-dashboard.service +``` + +### 5. Iniciar los servicios +```bash +sudo systemctl start adn-bridge.service +sudo systemctl start adn-dashboard.service +``` + +## Comandos Útiles + +### Ver estado de los servicios +```bash +sudo systemctl status adn-bridge.service +sudo systemctl status adn-dashboard.service +``` + +### Ver logs en tiempo real +```bash +sudo journalctl -u adn-bridge.service -f +sudo journalctl -u adn-dashboard.service -f +``` + +### Reiniciar servicios +```bash +sudo systemctl restart adn-bridge.service +sudo systemctl restart adn-dashboard.service +``` + +### Detener servicios +```bash +sudo systemctl stop adn-bridge.service +sudo systemctl stop adn-dashboard.service +``` + +### Deshabilitar arranque automático +```bash +sudo systemctl disable adn-bridge.service +sudo systemctl disable adn-dashboard.service +``` + +## Notas + +- Los servicios asumen que el proyecto está instalado en `/opt/adn-dmr` +- Si usas otra ruta, edita `WorkingDirectory` en los archivos .service +- El dashboard escucha en el puerto 5000 +- Asegúrate de tener las dependencias Python instaladas antes de iniciar diff --git a/docs/systemd/adn-bridge.service b/docs/systemd/adn-bridge.service new file mode 100644 index 0000000..957be4a --- /dev/null +++ b/docs/systemd/adn-bridge.service @@ -0,0 +1,16 @@ +[Unit] +Description=ADN Systems DMR Bridge Master - Servidor DMR principal +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/adn-dmr +ExecStart=/usr/bin/python3 bridge_master.py -c ./config/adn.cfg +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/hblink.py b/hblink.py index 106fe83..8647d49 100755 --- a/hblink.py +++ b/hblink.py @@ -65,9 +65,58 @@ from urllib.request import urlopen import shutil import csv +import json +import os import math +from password_crypto import decrypt_password + +USER_PASSWORDS = {} +USER_PASSWORDS_FILE = 'data/user_passwords.json' +USER_PASSWORDS_LAST_LOAD = 0 +USER_PASSWORDS_RELOAD_INTERVAL = 10 + +def load_user_passwords(): + global USER_PASSWORDS, USER_PASSWORDS_LAST_LOAD + current_time = time() + if current_time - USER_PASSWORDS_LAST_LOAD < USER_PASSWORDS_RELOAD_INTERVAL: + return USER_PASSWORDS + try: + if os.path.exists(USER_PASSWORDS_FILE): + with open(USER_PASSWORDS_FILE, 'r') as f: + data = json.load(f) + encrypted_passwords = data.get('passwords', {}) + USER_PASSWORDS = {} + for radio_id, pwd in encrypted_passwords.items(): + USER_PASSWORDS[radio_id] = decrypt_password(pwd) + logger.debug('(AUTH) Loaded %d individual passwords from %s', len(USER_PASSWORDS), USER_PASSWORDS_FILE) + else: + USER_PASSWORDS = {} + except (FileNotFoundError, json.JSONDecodeError) as e: + logger.warning('(AUTH) Could not load user passwords: %s', e) + USER_PASSWORDS = {} + USER_PASSWORDS_LAST_LOAD = current_time + return USER_PASSWORDS + +def get_user_password(radio_id): + passwords = load_user_passwords() + radio_id_str = str(radio_id) + + if not radio_id_str.isdigit(): + return None + + if radio_id_str in passwords: + return passwords[radio_id_str].encode('utf-8') + + if len(radio_id_str) == 9: + base_id = radio_id_str[:7] + if base_id in passwords: + logger.debug('(AUTH) Radio ID %s using base ID %s password', radio_id_str, base_id) + return passwords[base_id].encode('utf-8') + + return None + logging.TRACE = 5 logging.addLevelName(logging.TRACE, 'TRACE') @@ -972,20 +1021,33 @@ class HBSYSTEM(DatagramProtocol): _this_peer['LAST_PING'] = time() _sent_hash = _data[8:] _salt_str = bytes_4(_this_peer['SALT']) - if self._CONFIG['GLOBAL']['ALLOW_NULL_PASSPHRASE'] and len(self._config['PASSPHRASE']) == 0: - _this_peer['CONNECTION'] = 'WAITING_CONFIG' - self.send_peer(_peer_id, b''.join([RPTACK, _peer_id])) - logger.info('(%s) Peer %s has completed the login exchange successfully', self._system, _this_peer['RADIO_ID']) - else: + _radio_id_int = int_id(_peer_id) + _individual_password = get_user_password(_radio_id_int) + + if _individual_password is not None: + _calc_hash = bhex(sha256(_salt_str + _individual_password).hexdigest()) + if _sent_hash == _calc_hash: + _this_peer['CONNECTION'] = 'WAITING_CONFIG' + self.send_peer(_peer_id, b''.join([RPTACK, _peer_id])) + logger.info('(%s) Peer %s has completed the login exchange successfully (individual password)', self._system, _this_peer['RADIO_ID']) + else: + logger.warning('(%s) Peer %s has FAILED the login exchange (wrong individual password)', self._system, _this_peer['RADIO_ID']) + self.transport.write(b''.join([MSTNAK, _peer_id]), _sockaddr) + del self._peers[_peer_id] + elif len(self._config['PASSPHRASE']) > 0: _calc_hash = bhex(sha256(_salt_str+self._config['PASSPHRASE']).hexdigest()) if _sent_hash == _calc_hash: _this_peer['CONNECTION'] = 'WAITING_CONFIG' self.send_peer(_peer_id, b''.join([RPTACK, _peer_id])) - logger.info('(%s) Peer %s has completed the login exchange successfully', self._system, _this_peer['RADIO_ID']) + logger.info('(%s) Peer %s has completed the login exchange successfully (global passphrase)', self._system, _this_peer['RADIO_ID']) else: - logger.info('(%s) Peer %s has FAILED the login exchange successfully', self._system, _this_peer['RADIO_ID']) + logger.warning('(%s) Peer %s has FAILED the login exchange (wrong global passphrase)', self._system, _this_peer['RADIO_ID']) self.transport.write(b''.join([MSTNAK, _peer_id]), _sockaddr) del self._peers[_peer_id] + else: + logger.warning('(%s) Peer %s has FAILED - no individual password configured and no global passphrase', self._system, _this_peer['RADIO_ID']) + self.transport.write(b''.join([MSTNAK, _peer_id]), _sockaddr) + del self._peers[_peer_id] else: self.transport.write(b''.join([MSTNAK, _peer_id]), _sockaddr) logger.info('(%s) Login challenge from Radio ID that has not logged in: %s', self._system, int_id(_peer_id)) diff --git a/install.sh b/install.sh index c47b9db..bc9184a 100755 --- a/install.sh +++ b/install.sh @@ -2,5 +2,5 @@ # Install the required support programs apt-get install python3 python3-pip -y -pip3 install -r requirements.txt +pip3 install -r requirements.txt --break-system-packages diff --git a/install_debian13_arm.sh b/install_debian13_arm.sh new file mode 100644 index 0000000..280860c --- /dev/null +++ b/install_debian13_arm.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# +# Script de instalacion para ADN Systems DMR Peer Server +# Debian 13 (Trixie) - ARM64/ARMv7 +# +# Este script instala las dependencias necesarias para compilar +# y ejecutar el servidor DMR en sistemas ARM con Debian 13 +# + +set -e + +echo "==============================================" +echo " ADN Systems DMR Peer Server" +echo " Instalador para Debian 13 (Trixie) ARM" +echo "==============================================" +echo "" + +if [ "$EUID" -ne 0 ]; then + echo "Error: Este script debe ejecutarse como root (sudo)" + exit 1 +fi + +echo "[1/4] Actualizando repositorios..." +apt-get update + +echo "" +echo "[2/4] Instalando dependencias de compilacion..." +apt-get install -y \ + build-essential \ + python3-dev \ + python3-pip \ + python3-venv \ + libffi-dev \ + libssl-dev \ + git + +echo "" +echo "[3/4] Instalando dependencias de Python..." +pip3 install --break-system-packages -r requirements.txt + +echo "" +echo "[4/4] Verificando instalacion..." +python3 -c "import bitarray; import twisted; import flask; print('Todas las dependencias instaladas correctamente')" + +echo "" +echo "==============================================" +echo " Instalacion completada exitosamente" +echo "==============================================" +echo "" +echo "Para iniciar el servidor:" +echo " python3 bridge_master.py -c ./config/adn.cfg" +echo "" +echo "Para iniciar el dashboard:" +echo " python3 dashboard.py" +echo "" diff --git a/password_crypto.py b/password_crypto.py new file mode 100644 index 0000000..542c11f --- /dev/null +++ b/password_crypto.py @@ -0,0 +1,48 @@ +from cryptography.fernet import Fernet +import os +import base64 +import hashlib + +ENCRYPTION_KEY_FILE = 'config/encryption_key.secret' + +def get_or_create_key(): + if os.path.exists(ENCRYPTION_KEY_FILE): + with open(ENCRYPTION_KEY_FILE, 'rb') as f: + return f.read() + else: + key = Fernet.generate_key() + os.makedirs(os.path.dirname(ENCRYPTION_KEY_FILE), exist_ok=True) + with open(ENCRYPTION_KEY_FILE, 'wb') as f: + f.write(key) + return key + +def get_fernet(): + key = get_or_create_key() + return Fernet(key) + +def encrypt_password(password): + if not password: + return password + fernet = get_fernet() + encrypted = fernet.encrypt(password.encode('utf-8')) + return encrypted.decode('utf-8') + +def decrypt_password(encrypted_password): + if not encrypted_password: + return encrypted_password + try: + fernet = get_fernet() + decrypted = fernet.decrypt(encrypted_password.encode('utf-8')) + return decrypted.decode('utf-8') + except Exception: + return encrypted_password + +def is_encrypted(value): + if not value: + return False + try: + fernet = get_fernet() + fernet.decrypt(value.encode('utf-8')) + return True + except Exception: + return False diff --git a/replit.md b/replit.md new file mode 100644 index 0000000..3c3c28e --- /dev/null +++ b/replit.md @@ -0,0 +1,209 @@ +# ADN Systems DMR Peer Server + +## Overview + +ADN Systems DMR Peer Server is a fork of FreeDMR, implementing a Digital Mobile Radio (DMR) network server. Launched in April 2024 by international amateur radio enthusiasts, it operates on an Open Bridge Protocol (OBP) fostering a decentralized network architecture. The system handles DMR voice and data communication, acting as a conference bridge/reflector that routes traffic between connected systems (repeaters, hotspots, peers) based on configurable bridge rules. + +## User Preferences + +Preferred communication style: Simple, everyday language. + +## System Architecture + +### Core Protocol Implementation + +**Problem**: Need to implement HomeBrew Repeater Protocol (HBP) and Open Bridge Protocol for DMR communication +**Solution**: Python-based protocol handlers using Twisted framework for asynchronous networking +- `hblink.py` serves as the core protocol engine implementing HBSYSTEM and OPENBRIDGE classes +- Supports both peer and master/server modes for network topology flexibility +- Uses DatagramProtocol for UDP-based DMR packet handling +- Implements custom authentication using SHA256/BLAKE2b hashing with HMAC for security + +**Rationale**: Twisted provides battle-tested async I/O suitable for handling multiple simultaneous connections with low latency requirements critical for voice communication. + +### Bridge/Routing Architecture + +**Problem**: Route voice traffic between multiple DMR systems based on talkgroups and timeslots +**Solution**: Conference bridge pattern implemented in `bridge_master.py` and `bridge.py` +- Traffic routing based on rules defined in `rules.py` configuration +- Systems can be dynamically activated/deactivated on conference bridges +- Supports timeout-based automatic disconnection +- Static routing option available for permanent connections + +**Alternatives Considered**: End-to-end routing was rejected in favor of conference bridge approach for better scalability and simpler management. + +**Pros**: Flexible routing, easy management, scalable +**Cons**: Systems must individually join bridges (not transparent end-to-end) + +### Proxy Architecture + +**Problem**: Allow multiple hotspots/repeaters to connect through a single public IP address +**Solution**: UDP proxy implementation in `hotspot_proxy_v2.py` and `hotspot_proxy_self_service.py` +- Dynamic port allocation for incoming connections +- Connection tracking with timeout-based cleanup +- Blacklist support for access control +- Self-service variant includes database-backed client management + +**Rationale**: Many amateur radio operators are behind NAT/firewalls; proxy enables connectivity without port forwarding. + +### Voice Synthesis System + +**Problem**: Generate voice announcements for system events (linking, unlinking, status) +**Solution**: AMBE (Advanced Multi-Band Excitation) voice codec integration +- Pre-recorded indexed voice files in multiple languages (en_GB, es_ES, fr_FR, etc.) +- `read_ambe.py` handles reading/parsing of AMBE audio files +- `mk_voice.py` generates HBP-compliant voice packets from AMBE data +- `playback.py` and `play_ambe.py` handle voice injection into streams + +**Rationale**: Provides accessible feedback to users without requiring external TTS systems, maintains compatibility with DMR audio codecs. + +### Individual Password Authentication + +**Problem**: Need individual password authentication per Radio ID (indicativo) for enhanced security +**Solution**: JSON-based password storage with automatic reload +- Password file: `data/user_passwords.json` stores individual passwords by Radio ID +- Web dashboard: `dashboard.py` provides admin interface for password management +- Auto-reload: Passwords are reloaded every 10 seconds without server restart +- Fallback: Global passphrase used if no individual password is configured +- Mandatory authentication: Individual or global password required (no null passphrase allowed) + +**Suffix Support for Radio IDs**: +- Radio IDs of 9 digits (ej: 214501601-214501699) can use the password of their 7-digit base ID (ej: 2145016) +- Useful for users with multiple hotspots/devices using suffix extensions +- Priority: Exact ID match first, then base ID match + +**Authentication Priority**: +1. Individual password for exact Radio ID (if configured) +2. Individual password for base ID (7 digits, for 9-digit IDs with suffix) +3. Global passphrase (if configured) +4. Reject connection (no valid credentials) + +**Dashboard Credentials**: Stored in `config/dashboard_credentials.json` file (not uploaded to GitHub) +- Supports multiple administrators with the following format: +```json +{ + "admins": [ + {"user": "admin", "password": "admin123"}, + {"user": "operador1", "password": "clave456"} + ] +} +``` +- Also supports legacy single-admin format for backwards compatibility + +**Dashboard Web con Acceso Dual**: +- Pagina principal (`/`): Selector entre acceso usuario y administrador +- Acceso Usuario (`/user/login`): Login con Radio ID y contrasena para cambiar su propia contrasena +- Acceso Administrador (`/admin/login`): Login para gestionar todas las contrasenas +- Los usuarios pueden cambiar su contrasena sin necesitar al administrador + +**Busqueda por Indicativo (Callsign)**: +- Descarga automatica diaria de la base de datos de RadioID.net (`data/user.csv`) +- Formato CSV con campos: RADIO_ID, CALLSIGN, FIRST_NAME, LAST_NAME, CITY, STATE, COUNTRY +- Funcion de busqueda en el panel de administracion para encontrar Radio IDs por indicativo +- Muestra toda la informacion del registro: nombre, ciudad, estado, pais +- Soporta busqueda parcial (ej: "EA4" muestra todos los indicativos que empiezan por EA4) +- Limita resultados a 20 para mejor rendimiento +- Boton "Usar ID" para autorellenar el formulario de agregar contrasena + +### Servidor de Seguridad Centralizado (Rama: descentralizada) + +**Problema**: Gestionar credenciales de forma centralizada desde un servidor remoto +**Solucion**: Descarga automatica de archivos de seguridad desde servidor central + +**Configuracion en adn.cfg seccion [GLOBAL]**: +- `URL_SECURITY`: IP o DNS del servidor central de seguridad +- `PORT_SECURITY`: Puerto del servidor central +- `PASS_SECURITY`: Contrasena de autenticacion del administrador +- `USERS_PASS`: Nombre del archivo de contrasenas (default: user_passwords.json) +- `HASH_ENCRYPT`: Nombre del archivo de clave de encriptacion (default: encryption_key.secret) + +**Comportamiento de Descarga**: +- `encryption_key.secret`: Se descarga SOLO al arrancar el servidor, sobrescribe el existente +- `user_passwords.json`: Se descarga cada 5 minutos, compara contenido antes de actualizar + +**Sintaxis de Descarga (curl)**: +``` +curl -L "http://URL_SECURITY:PORT_SECURITY/descargar?pass=PASS_SECURITY&encryption_key.secret" +curl -L "http://URL_SECURITY:PORT_SECURITY/descargar?pass=PASS_SECURITY&user_passwords.json" +``` + +**Seguridad y Tolerancia a Fallos**: +- Si la descarga falla, se conservan los archivos locales existentes +- No se permite contrasena nula (ALLOW_NULL_PASSPHRASE eliminado) +- Autenticacion obligatoria: contrasena individual o global requerida + +**Archivos Descargados**: +- `config/encryption_key.secret`: Clave de encriptacion para descifrar contrasenas +- `data/user_passwords.json`: Archivo JSON con contrasenas encriptadas por Radio ID + +### Configuration Management + +**Problem**: Complex multi-system configuration with ACLs, bridges, and network parameters +**Solution**: INI-based configuration parsed by `config.py` +- Centralized configuration in `config/adn.cfg` +- ACL (Access Control List) processing for registration and talkgroup filtering +- Support for both whitelist and blacklist approaches +- Language preferences and multi-language support via `languages.py` + +### Monitoring and Reporting + +**Problem**: Need visibility into system operation, connections, and traffic +**Solution**: Network-based reporting protocol +- `report_receiver.py` and `report_sql.py` consume events from the core server +- Pickle-based serialization for config/bridge state transmission +- Opcode-based protocol defined in `reporting_const.py` +- SQL integration option for persistent event logging + +**Rationale**: Separates monitoring concerns from core routing logic, enables multiple monitoring tools. + +### Asterisk Integration + +**Problem**: Integration with Asterisk PBX for advanced features (app_rpt) +**Solution**: Asterisk Manager Interface (AMI) client in `AMI.py` +- Sends RPT (repeater) commands to Asterisk +- Uses Twisted LineReceiver for line-based protocol handling +- Supports node-specific command routing + +### API Layer + +**Problem**: External systems need programmatic access to server functions +**Solution**: XML-RPC and SOAP API implementation +- `API.py` implements Spyne-based SOAP service (FD_API) +- Peer validation and authentication endpoints +- `api_client.py` provides example XML-RPC client +- Key-based authentication for peer systems + +## External Dependencies + +### Network Protocols +- **Twisted** (>= 16.3.0) - Asynchronous networking framework for all protocol handling +- **dmr_utils3** (>= 0.1.19) - DMR protocol utilities for packet encoding/decoding, BPTC, Golay error correction + +### Database Systems +- **MySQL/MariaDB** - Via twisted.enterprise.adbapi and MySQLdb + - Used by `proxy_db.py` for self-service proxy client management + - Stores client registrations, authentication, and connection tracking + - Optional for report_sql.py event logging + +### Remote Object Communication +- **Pyro5** - Python Remote Objects for inter-process communication + - Used by proxy services for distributed architecture + - Enables separation of proxy components across processes/hosts + +### Third-Party Services +- **radioid.net API** - DMR ID database lookups + - `peer_ids.json`, `subscriber_ids.json`, `talkgroup_ids.json` downloaded from external sources + - Cached locally with staleness checking via `utils.py::try_download()` + - Provides callsign/name resolution for DMR IDs + +### Supporting Libraries +- **bitarray/bitstring** - Binary data manipulation for DMR packet construction +- **configparser** - INI file parsing for hblink.cfg +- **setproctitle** - Process naming for easier system monitoring +- **resettabletimer** - Timeout management for connection tracking +- **hashlib/hmac** - Cryptographic functions for authentication + +### Language/Voice Assets +- Pre-recorded AMBE voice files in Audio/ directory +- Multiple language support (en_GB, es_ES, fr_FR, de_DE, dk_DK, it_IT, no_NO, pl_PL, se_SE, pt_PT, cy_GB, el_GR, th_TH, CW) +- Voice file indexing via i8n_voice_map.py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index de11f31..42576ad 100755 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,8 @@ resettabletimer>=0.7.0 setproctitle Pyro5 spyne +flask +mysql-connector-python +cryptography +markdown +pdfkit diff --git a/security_downloader.py b/security_downloader.py new file mode 100644 index 0000000..79b52de --- /dev/null +++ b/security_downloader.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +import os +import logging +import urllib.request +import urllib.error +import tempfile +import shutil +import socket +from time import time + +logger = logging.getLogger(__name__) + +DOWNLOAD_INTERVAL_PASSWORDS = 300 +DOWNLOAD_INTERVAL_ENCRYPTION = None + +_last_passwords_download = 0 +_last_passwords_size = 0 +_last_passwords_content = None + +def resolve_hostname(hostname, timeout=10): + try: + old_timeout = socket.getdefaulttimeout() + socket.setdefaulttimeout(timeout) + ip = socket.gethostbyname(hostname) + socket.setdefaulttimeout(old_timeout) + logger.debug('(SECURITY) Resolved %s to %s', hostname, ip) + return ip + except socket.gaierror as e: + logger.error('(SECURITY) DNS resolution failed for %s: %s', hostname, str(e)) + return None + except Exception as e: + logger.error('(SECURITY) Unexpected error resolving %s: %s', hostname, str(e)) + return None + +def build_download_url(config, filename): + url_security = config['GLOBAL'].get('URL_SECURITY', '').strip() + port_security = config['GLOBAL'].get('PORT_SECURITY', '').strip() + pass_security = config['GLOBAL'].get('PASS_SECURITY', '').strip() + + if not url_security or not port_security or not pass_security: + return None, None + + try: + socket.inet_aton(url_security) + host = url_security + except socket.error: + host = resolve_hostname(url_security) + if not host: + logger.error('(SECURITY) Could not resolve hostname: %s', url_security) + return None, None + + url = f"http://{host}:{port_security}/descargar?pass={pass_security}&file={filename}" + return url, url_security + +def download_file_safely(url, dest_path, timeout=60): + try: + temp_fd, temp_path = tempfile.mkstemp() + os.close(temp_fd) + + try: + logger.debug('(SECURITY) Attempting download from: %s', url) + req = urllib.request.Request(url) + req.add_header('User-Agent', 'ADN-Systems-DMR/1.0') + + with urllib.request.urlopen(req, timeout=timeout) as response: + content = response.read() + + if len(content) == 0: + logger.warning('(SECURITY) Downloaded file is empty, keeping existing: %s', dest_path) + os.unlink(temp_path) + return False + + with open(temp_path, 'wb') as f: + f.write(content) + + os.makedirs(os.path.dirname(dest_path), exist_ok=True) + shutil.move(temp_path, dest_path) + logger.info('(SECURITY) Successfully downloaded: %s (%d bytes)', dest_path, len(content)) + return True + + except urllib.error.HTTPError as e: + logger.error('(SECURITY) HTTP error downloading %s: %s (Code: %d)', dest_path, str(e), e.code) + if os.path.exists(temp_path): + os.unlink(temp_path) + return False + except urllib.error.URLError as e: + logger.error('(SECURITY) URL error downloading %s: %s', dest_path, str(e.reason)) + if os.path.exists(temp_path): + os.unlink(temp_path) + return False + except socket.timeout: + logger.error('(SECURITY) Timeout downloading %s', dest_path) + if os.path.exists(temp_path): + os.unlink(temp_path) + return False + + except Exception as e: + logger.error('(SECURITY) Unexpected error downloading %s: %s', dest_path, str(e)) + return False + +def download_encryption_key(config): + hash_encrypt = config['GLOBAL'].get('HASH_ENCRYPT', 'encryption_key.secret').strip() + dest_path = os.path.join('config', hash_encrypt) + + url, original_host = build_download_url(config, hash_encrypt) + if not url: + logger.debug('(SECURITY) Security server not configured, skipping encryption key download') + return False + + logger.info('(SECURITY) Downloading encryption key from central server...') + return download_file_safely(url, dest_path) + +def download_user_passwords(config, force=False): + global _last_passwords_download, _last_passwords_size, _last_passwords_content + + current_time = time() + if not force and (current_time - _last_passwords_download) < DOWNLOAD_INTERVAL_PASSWORDS: + return False + + users_pass = config['GLOBAL'].get('USERS_PASS', 'user_passwords.json').strip() + dest_path = os.path.join('data', users_pass) + + url, original_host = build_download_url(config, users_pass) + if not url: + logger.debug('(SECURITY) Security server not configured, skipping passwords download') + return False + + try: + logger.debug('(SECURITY) Downloading passwords from: %s', url) + req = urllib.request.Request(url) + req.add_header('User-Agent', 'ADN-Systems-DMR/1.0') + + with urllib.request.urlopen(req, timeout=60) as response: + new_content = response.read() + new_size = len(new_content) + + if new_size == 0: + logger.warning('(SECURITY) Downloaded passwords file is empty, keeping existing') + _last_passwords_download = current_time + return False + + if _last_passwords_content is not None: + if new_content == _last_passwords_content: + logger.debug('(SECURITY) Passwords file unchanged, no update needed') + _last_passwords_download = current_time + return False + + temp_fd, temp_path = tempfile.mkstemp() + os.close(temp_fd) + + with open(temp_path, 'wb') as f: + f.write(new_content) + + os.makedirs(os.path.dirname(dest_path), exist_ok=True) + shutil.move(temp_path, dest_path) + + _last_passwords_content = new_content + _last_passwords_size = new_size + _last_passwords_download = current_time + + logger.info('(SECURITY) Successfully updated passwords file: %s (%d bytes)', dest_path, new_size) + return True + + except urllib.error.HTTPError as e: + logger.error('(SECURITY) HTTP error downloading passwords: %s (Code: %d)', str(e), e.code) + _last_passwords_download = current_time + return False + except urllib.error.URLError as e: + logger.error('(SECURITY) URL error downloading passwords: %s', str(e.reason)) + _last_passwords_download = current_time + return False + except socket.timeout: + logger.error('(SECURITY) Timeout downloading passwords') + _last_passwords_download = current_time + return False + except Exception as e: + logger.error('(SECURITY) Unexpected error downloading passwords: %s', str(e)) + _last_passwords_download = current_time + return False + +def init_security_downloads(config): + url_security = config['GLOBAL'].get('URL_SECURITY', '').strip() + if not url_security: + logger.info('(SECURITY) Central security server not configured') + return False + + port_security = config['GLOBAL'].get('PORT_SECURITY', '').strip() + + logger.info('(SECURITY) Initializing centralized security downloads...') + logger.info('(SECURITY) Security server: %s:%s', url_security, port_security) + + try: + socket.inet_aton(url_security) + logger.info('(SECURITY) Using IP address: %s', url_security) + except socket.error: + resolved_ip = resolve_hostname(url_security) + if resolved_ip: + logger.info('(SECURITY) Resolved hostname %s to IP: %s', url_security, resolved_ip) + else: + logger.error('(SECURITY) Failed to resolve hostname: %s', url_security) + return False + + download_encryption_key(config) + + download_user_passwords(config, force=True) + + return True + +def periodic_password_download(config): + download_user_passwords(config) diff --git a/systemd-scripts/adn.service b/systemd-scripts/adn.service index 6dff3b6..c6d4dc6 100644 --- a/systemd-scripts/adn.service +++ b/systemd-scripts/adn.service @@ -5,7 +5,7 @@ After=syslog.target network.target [Service] User=root WorkingDirectory=/opt/adn -ExecStart=/usr/bin/python3 bridge_master.py -c ./config/adn.cfg -r ./config/rules.py +ExecStart=/usr/bin/python3 bridge_master.py -c ./config/adn.cfg [Install] WantedBy=multi-user.target