diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 index 192fd9b..83e7b68 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,29 @@ +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -88,7 +114,6 @@ ENV/ # Rope project settings .ropeproject -.DS_Store hblink.cfg *.config *.bak @@ -98,3 +123,18 @@ local_subscriber_ids.* peer_ids.* local_peer_ids.* talkgroup_ids.* + + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix diff --git a/Audio/CW/adn.ambe b/Audio/CW/adn.ambe new file mode 100644 index 0000000..b7f6fd1 Binary files /dev/null and b/Audio/CW/adn.ambe differ diff --git a/Audio/CW/freedmr.ambe b/Audio/CW/freedmr.ambe deleted file mode 100644 index e56b8d6..0000000 Binary files a/Audio/CW/freedmr.ambe and /dev/null differ diff --git a/Audio/cy_GB/adn.ambe b/Audio/cy_GB/adn.ambe new file mode 100644 index 0000000..fea8c88 Binary files /dev/null and b/Audio/cy_GB/adn.ambe differ diff --git a/Audio/cy_GB/freedmr.ambe b/Audio/cy_GB/freedmr.ambe deleted file mode 100644 index 41d0271..0000000 Binary files a/Audio/cy_GB/freedmr.ambe and /dev/null differ diff --git a/Audio/de_DE/adn.ambe b/Audio/de_DE/adn.ambe new file mode 100644 index 0000000..fea8c88 Binary files /dev/null and b/Audio/de_DE/adn.ambe differ diff --git a/Audio/el_GR/adn.ambe b/Audio/el_GR/adn.ambe new file mode 100644 index 0000000..fea8c88 Binary files /dev/null and b/Audio/el_GR/adn.ambe differ diff --git a/Audio/el_GR/freedmr.ambe b/Audio/el_GR/freedmr.ambe deleted file mode 100644 index 828312b..0000000 Binary files a/Audio/el_GR/freedmr.ambe and /dev/null differ diff --git a/Audio/en_GB/adn.ambe b/Audio/en_GB/adn.ambe new file mode 100644 index 0000000..fea8c88 Binary files /dev/null and b/Audio/en_GB/adn.ambe differ diff --git a/Audio/en_GB/freedmr.ambe b/Audio/en_GB/freedmr.ambe deleted file mode 100644 index d6d3f22..0000000 Binary files a/Audio/en_GB/freedmr.ambe and /dev/null differ diff --git a/Audio/en_GB_2/adn.ambe b/Audio/en_GB_2/adn.ambe new file mode 100644 index 0000000..fea8c88 Binary files /dev/null and b/Audio/en_GB_2/adn.ambe differ diff --git a/Audio/en_GB_2/freedmr.ambe b/Audio/en_GB_2/freedmr.ambe deleted file mode 100644 index d6d3f22..0000000 Binary files a/Audio/en_GB_2/freedmr.ambe and /dev/null differ diff --git a/Audio/es_ES/adn.ambe b/Audio/es_ES/adn.ambe new file mode 100644 index 0000000..fea8c88 Binary files /dev/null and b/Audio/es_ES/adn.ambe differ diff --git a/Audio/es_ES/freedmr.ambe b/Audio/es_ES/freedmr.ambe deleted file mode 100644 index be242e9..0000000 Binary files a/Audio/es_ES/freedmr.ambe and /dev/null differ diff --git a/Audio/fr_FR/adn.ambe b/Audio/fr_FR/adn.ambe new file mode 100644 index 0000000..fea8c88 Binary files /dev/null and b/Audio/fr_FR/adn.ambe differ diff --git a/Audio/fr_FR/freedmr.ambe b/Audio/fr_FR/freedmr.ambe deleted file mode 100644 index 41bff87..0000000 Binary files a/Audio/fr_FR/freedmr.ambe and /dev/null differ diff --git a/Audio/pt_PT/adn.ambe b/Audio/pt_PT/adn.ambe new file mode 100644 index 0000000..fea8c88 Binary files /dev/null and b/Audio/pt_PT/adn.ambe differ diff --git a/Audio/pt_PT/freedmr.ambe b/Audio/pt_PT/freedmr.ambe deleted file mode 100644 index cf1601e..0000000 Binary files a/Audio/pt_PT/freedmr.ambe and /dev/null differ diff --git a/Audio/th_TH/adn.ambe b/Audio/th_TH/adn.ambe new file mode 100644 index 0000000..fea8c88 Binary files /dev/null and b/Audio/th_TH/adn.ambe differ diff --git a/Audio/th_TH/freedmr.ambe b/Audio/th_TH/freedmr.ambe deleted file mode 100644 index 53b634c..0000000 Binary files a/Audio/th_TH/freedmr.ambe and /dev/null differ diff --git a/FreeDMR-MINIMAL.cfg b/FreeDMR-MINIMAL.cfg deleted file mode 100755 index 383fc08..0000000 --- a/FreeDMR-MINIMAL.cfg +++ /dev/null @@ -1,43 +0,0 @@ -#This empty config file will use defaults for everything apart from OBP and HBP config -#This is usually a sensible choice. - - -[GLOBAL] -SERVER_ID: 0000 -DEBUG_BRIDGES: True - -[REPORTS] - -[LOGGER] - -[ALIASES] - -[ALLSTAR] - -[SYSTEM] -MODE: MASTER -ENABLED: True -REPEAT: True -MAX_PEERS: 1 -EXPORT_AMBE: False -IP: 127.0.0.1 -PORT: 54000 -PASSPHRASE: -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 -TS1_STATIC: -TS2_STATIC: -DEFAULT_REFLECTOR: 0 -SINGLE_MODE: True -VOICE_IDENT: True -ANNOUNCEMENT_LANGUAGE: en_GB -GENERATOR: 100 -ALLOW_UNREG_ID: False -PROXY_CONTROL: True -OVERRIDE_IDENT_TG: - diff --git a/README.md b/README.md index c2d88e7..2d06544 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -### FreeDMR Peer Server is a (now very diverged) fork of the hblink3 project ### +### ADN Systems DMR Peer Server is a fork from FreeDMR by G7RZU hacknix ### -FreeDMR Peer Server - software to assist in building a peer mesh network - -Please see the wiki for documentation. +Launched on April 21, 2024, by a visionary group of 20 international amateur radio enthusiasts, +ADN Systems operates on an Open Bridge Protocol (OBP), fostering a decentralized network +devoid of hierarchical structures. diff --git a/bridge.py b/bridge.py index 86d6dc3..b38fc4e 100755 --- a/bridge.py +++ b/bridge.py @@ -655,7 +655,7 @@ class routerHBP(HBSYSTEM): self.STATUS[_slot]['lastSeq'] = _seq #Save this packet self.STATUS[_slot]['lastData'] = _data - + for _bridge in BRIDGES: diff --git a/bridge_master.py b/bridge_master.py index e8fb2a6..2edc20f 100644 --- a/bridge_master.py +++ b/bridge_master.py @@ -821,7 +821,7 @@ def ident(): _say.append(words[_lang]['silence']) _say.append(words[_lang]['silence']) - _say.append(words[_lang]['freedmr']) + _say.append(words[_lang]['adn']) #test #_say.append(AMBEobj.readSingleFile('alpha.ambe')) diff --git a/config.py b/config.py index b90081f..83bbf1c 100755 --- a/config.py +++ b/config.py @@ -96,7 +96,7 @@ def acl_build(_acl, _max): if (const.ID_MIN <= id <= _max): acl.append((id, id)) else: - sys.exit('ACL CREATION ERROR, VALUE OUT OF RANGE ({} - {}) IN SINGLE ID ENTRY: {}'.format(const.ID_MIN, _max, entry)) + sys.exit('ACL CREATION ERROR, VALUE OUT OF RANGE ({} - {}) IN SINGLE ID ENTRY: {}'.format(const.ID_MIN, _max, entry)) return (action, acl) @@ -176,19 +176,19 @@ def build_config(_config_file): elif section == 'ALIASES': CONFIG['ALIASES'].update({ 'TRY_DOWNLOAD': config.getboolean(section, 'TRY_DOWNLOAD', fallback=True), - 'PATH': config.get(section, 'PATH', fallback='./json/'), + 'PATH': config.get(section, 'PATH', fallback='./data/'), 'PEER_FILE': config.get(section, 'PEER_FILE', fallback='peer_ids.json'), 'SUBSCRIBER_FILE': config.get(section, 'SUBSCRIBER_FILE', fallback='subscriber_ids.json'), 'TGID_FILE': config.get(section, 'TGID_FILE', fallback='talkgroup_ids.json'), - 'PEER_URL': config.get(section, 'PEER_URL', fallback='https://freedmr-lh.gb7fr.org.uk/json/peer_ids.json'), - 'SUBSCRIBER_URL': config.get(section, 'SUBSCRIBER_URL', fallback='https://freedmr-lh.gb7fr.org.uk/json/subscriber_ids.json'), - 'TGID_URL': config.get(section, 'TGID_URL', fallback='https://freedmr-lh.gb7fr.org.uk/json/talkgroup_ids.json'), + 'PEER_URL': config.get(section, 'PEER_URL', fallback='https://adn.systems/files/peer_ids.json'), + 'SUBSCRIBER_URL': config.get(section, 'SUBSCRIBER_URL', fallback='https://adn.systems/files/subscriber_ids.json'), + 'TGID_URL': config.get(section, 'TGID_URL', fallback='https://adn.systems/files/talkgroup_ids.json'), 'STALE_TIME': config.getint(section, 'STALE_DAYS', fallback=1) * 86400, 'SUB_MAP_FILE': config.get(section, 'SUB_MAP_FILE', fallback='sub_map.pkl'), 'LOCAL_SUBSCRIBER_FILE': config.get(section, 'LOCAL_SUBSCRIBER_FILE', fallback='local_subscribers.json'), - 'SERVER_ID_URL': config.get(section, 'SERVER_ID_URL', fallback='https://freedmr-lh.gb7fr.org.uk/json/server_ids.tsv'), + 'SERVER_ID_URL': config.get(section, 'SERVER_ID_URL', fallback='https://adn.systems/files/server_ids.tsv'), 'SERVER_ID_FILE': config.get(section, 'SERVER_ID_FILE', fallback='server_ids.tsv'), - 'CHECKSUM_URL': config.get(section, 'CHECKSUM_URL', fallback='https://freedmr-lh.gb7fr.org.uk/file_checksums.json'), + 'CHECKSUM_URL': config.get(section, 'CHECKSUM_URL', fallback='https://adn.systems/files/file_checksums.json'), 'CHECKSUM_FILE': config.get(section, 'CHECKSUM_FILE', fallback='file_checksums.json'), 'KEYS_FILE': config.get(section, 'KEYS_FILE', fallback='keys.json') }) @@ -310,7 +310,7 @@ def build_config(_config_file): 'REPEAT': config.getboolean(section, 'REPEAT', fallback=True), 'MAX_PEERS': config.getint(section, 'MAX_PEERS', fallback=1), 'IP': config.get(section, 'IP', fallback='127.0.0.1'), - 'PORT': config.getint(section, 'PORT', fallback=54000), + 'PORT': config.getint(section, 'PORT', fallback=56400), 'PASSPHRASE': bytes(config.get(section, 'PASSPHRASE', fallback=''), 'utf-8'), 'GROUP_HANGTIME': config.getint(section, 'GROUP_HANGTIME',fallback=5), 'USE_ACL': config.getboolean(section, 'USE_ACL', fallback=False), @@ -327,7 +327,7 @@ def build_config(_config_file): 'GENERATOR': config.getint(section, 'GENERATOR', fallback=100), 'ANNOUNCEMENT_LANGUAGE': config.get(section, 'ANNOUNCEMENT_LANGUAGE', fallback='en_GB'), 'ALLOW_UNREG_ID': config.getboolean(section,'ALLOW_UNREG_ID', fallback=False), - 'PROXY_CONTROL' : config.getboolean(section,'PROXY_CONTROL', fallback=True), + 'PROXY_CONTROL' : config.getboolean(section,'PROXY_CONTROL', fallback=False), 'OVERRIDE_IDENT_TG': config.get(section, 'OVERRIDE_IDENT_TG', fallback=False) }}) CONFIG['SYSTEMS'][section].update({'PEERS': {}}) @@ -362,7 +362,7 @@ def build_config(_config_file): try: addr_info = socket.getaddrinfo(CONFIG['SYSTEMS'][section]['TARGET_IP'],CONFIG['SYSTEMS'][section]['TARGET_PORT'],socket.AF_INET6, socket.IPPROTO_IP) except gaierror: - addr_info = socket.getaddrinfo(CONFIG['SYSTEMS'][section]['TARGET_IP'],CONFIG['SYSTEMS'][section]['TARGET_PORT'],socket.AF_INET, socket.IPPROTO_IP) + addr_info = socket.getaddrinfo(CONFIG['SYSTEMS'][section]['TARGET_IP'],CONFIG['SYSTEMS'][section]['TARGET_PORT'],socket.AF_INET, socket.IPPROTO_IP) elif CONFIG['SYSTEMS'][section]['IP'] and IsIPv6Address(CONFIG['SYSTEMS'][section]['IP']): addr_info = socket.getaddrinfo(CONFIG['SYSTEMS'][section]['TARGET_IP'],CONFIG['SYSTEMS'][section]['TARGET_PORT'],socket.AF_INET6, socket.IPPROTO_IP) @@ -412,7 +412,7 @@ if __name__ == '__main__': # Ensure we have a path for the config file, if one wasn't specified, then use the execution directory if not cli_args.CONFIG_FILE: - cli_args.CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/freedmr.cfg' + cli_args.CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/config/adn.cfg' CONFIG = build_config(cli_args.CONFIG_FILE) pprint(CONFIG) diff --git a/config/ADN-MINIMAL.cfg b/config/ADN-MINIMAL.cfg new file mode 100755 index 0000000..3c8043a --- /dev/null +++ b/config/ADN-MINIMAL.cfg @@ -0,0 +1,75 @@ +#This empty config file will use defaults for everything apart from OBP and HBP config +#This is usually a sensible choice. + + +[GLOBAL] +SERVER_ID: 0000 +DEBUG_BRIDGES: True + +[REPORTS] + +[LOGGER] + +[ALIASES] + +[ALLSTAR] + +[SYSTEM] +MODE: MASTER +ENABLED: True +REPEAT: True +MAX_PEERS: 1 +EXPORT_AMBE: False +IP: 127.0.0.1 +PORT: 56400 +PASSPHRASE: +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 +TS1_STATIC: +TS2_STATIC: +DEFAULT_REFLECTOR: 0 +SINGLE_MODE: False +VOICE_IDENT: False +ANNOUNCEMENT_LANGUAGE: en_GB +GENERATOR: 100 +ALLOW_UNREG_ID: False +PROXY_CONTROL: False +OVERRIDE_IDENT_TG: + +[ECHO] +MODE: PEER +ENABLED: True +LOOSE: False +EXPORT_AMBE: False +IP: 127.0.0.1 +PORT: 54916 +MASTER_IP: 127.0.0.1 +MASTER_PORT: 54915 +PASSPHRASE: passw0rd +CALLSIGN: ECHO +RADIO_ID: 9990 +RX_FREQ: 449000000 +TX_FREQ: 444000000 +TX_POWER: 25 +COLORCODE: 1 +SLOTS: 1 +LATITUDE: 00.0000 +LONGITUDE: 000.0000 +HEIGHT: 0 +LOCATION: 9990 Parrot +DESCRIPTION: ECHO +URL: adn.systems +SOFTWARE_ID: 20170620 +PACKAGE_ID: MMDVM_ADN-Systems +GROUP_HANGTIME: 5 +OPTIONS: +USE_ACL: True +SUB_ACL: DENY:1 +TGID_TS1_ACL: PERMIT:ALL +TGID_TS2_ACL: PERMIT:ALL +ANNOUNCEMENT_LANGUAGE: en_GB diff --git a/FreeDMR-SAMPLE-commented.cfg b/config/ADN-SAMPLE-commented.cfg similarity index 93% rename from FreeDMR-SAMPLE-commented.cfg rename to config/ADN-SAMPLE-commented.cfg index 979a8f3..1c254e8 100755 --- a/FreeDMR-SAMPLE-commented.cfg +++ b/config/ADN-SAMPLE-commented.cfg @@ -91,10 +91,10 @@ REPORT_CLIENTS: 127.0.0.1 # CRITICAL - only serious events [LOGGER] -LOG_FILE: /tmp/hblink.log +LOG_FILE: /tmp/adn-dmr.log LOG_HANDLERS: console-timed LOG_LEVEL: DEBUG -LOG_NAME: FreeDMR +LOG_NAME: ADN # DOWNLOAD AND IMPORT SUBSCRIBER, PEER and TGID ALIASES # Ok, not the TGID, there's no master list I know of to download @@ -104,18 +104,20 @@ LOG_NAME: FreeDMR # download again. Don't be an ass and change this to less than a few days. [ALIASES] TRY_DOWNLOAD: True -PATH: ./ +PATH: ./data PEER_FILE: peer_ids.json SUBSCRIBER_FILE: subscriber_ids.json TGID_FILE: talkgroup_ids.json -PEER_URL: https://www.radioid.net/static/rptrs.json -SUBSCRIBER_URL: https://www.radioid.net/static/users.json -TGID_URL: http://downloads.freedmr.uk/downloads/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 LOCAL_SUBSCRIBER_FILE: local_subcriber_ids.json STALE_DAYS: 1 -SERVER_ID_URL: http://downloads.freedmr.uk/downloads/FreeDMR_Hosts.csv +SUB_MAP_FILE: +SERVER_ID_URL: https://adn.systems/files/server_ids.tsv SERVER_ID_FILE: server_ids.tsv - +CHECKSUM_URL: https://adn.systems/files/file_checksums.json +CHECKSUM_FILE: file_checksums.json #Control server shared allstar instance via dial / AMI [ALLSTAR] @@ -199,8 +201,8 @@ SUB_ACL: DENY:1 TGID_TS1_ACL: PERMIT:ALL TGID_TS2_ACL: PERMIT:ALL DEFAULT_UA_TIMER: 10 -SINGLE_MODE: True -VOICE_IDENT: True +SINGLE_MODE: False +VOICE_IDENT: False #the next three lines no longer have any effect TS1_STATIC: TS2_STATIC: @@ -208,4 +210,4 @@ DEFAULT_REFLECTOR: 0 ANNOUNCEMENT_LANGUAGE: en_GB GENERATOR: 1 ALLOW_UNREG_ID: False -PROXY_CONTROL: True +PROXY_CONTROL: False diff --git a/FreeDMR-SAMPLE.cfg b/config/ADN-SAMPLE.cfg similarity index 59% rename from FreeDMR-SAMPLE.cfg rename to config/ADN-SAMPLE.cfg index fbeb09e..913896f 100755 --- a/FreeDMR-SAMPLE.cfg +++ b/config/ADN-SAMPLE.cfg @@ -22,26 +22,26 @@ REPORT_PORT: 4321 REPORT_CLIENTS: 127.0.0.1 [LOGGER] -LOG_FILE: freedmr.log +LOG_FILE: adn.log LOG_HANDLERS: console-timed LOG_LEVEL: INFO -LOG_NAME: FreeDMR +LOG_NAME: ADN [ALIASES] TRY_DOWNLOAD: True -PATH: ./ +PATH: ./data PEER_FILE: peer_ids.json SUBSCRIBER_FILE: subscriber_ids.json TGID_FILE: talkgroup_ids.json -PEER_URL: https://freedmr-lh.gb7fr.org.uk/json/peer_ids.json -SUBSCRIBER_URL: https://freedmr-lh.gb7fr.org.uk/json/subscriber_ids.json -TGID_URL: https://freedmr-lh.gb7fr.org.uk/json/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 LOCAL_SUBSCRIBER_FILE: local_subcriber_ids.json STALE_DAYS: 1 SUB_MAP_FILE: -SERVER_ID_URL: https://freedmr-lh.gb7fr.org.uk/json/server_ids.tsv +SERVER_ID_URL: https://adn.systems/files/server_ids.tsv SERVER_ID_FILE: server_ids.tsv -CHECKSUM_URL: https://freedmr-lh.gb7fr.org.uk/file_checksums.json +CHECKSUM_URL: https://adn.systems/files/file_checksums.json CHECKSUM_FILE: file_checksums.json @@ -68,7 +68,7 @@ SUB_ACL: DENY:1 TGID_ACL: PERMIT:ALL RELAX_CHECKS: True ENHANCED_OBP: True -PROTO_VER: 2 +PROTO_VER: 5 [SYSTEM] MODE: MASTER @@ -77,7 +77,7 @@ REPEAT: True MAX_PEERS: 1 EXPORT_AMBE: False IP: 127.0.0.1 -PORT: 54000 +PORT: 56400 PASSPHRASE: GROUP_HANGTIME: 5 USE_ACL: True @@ -86,13 +86,46 @@ SUB_ACL: DENY:1 TGID_TS1_ACL: PERMIT:ALL TGID_TS2_ACL: PERMIT:ALL DEFAULT_UA_TIMER: 60 -SINGLE_MODE: True -VOICE_IDENT: True +SINGLE_MODE: False +VOICE_IDENT: False TS1_STATIC: TS2_STATIC: DEFAULT_REFLECTOR: 0 ANNOUNCEMENT_LANGUAGE: en_GB GENERATOR: 100 ALLOW_UNREG_ID: False -PROXY_CONTROL: True +PROXY_CONTROL: False OVERRIDE_IDENT_TG: + +[ECHO] +MODE: PEER +ENABLED: True +LOOSE: False +EXPORT_AMBE: False +IP: 127.0.0.1 +PORT: 54916 +MASTER_IP: 127.0.0.1 +MASTER_PORT: 54915 +PASSPHRASE: passw0rd +CALLSIGN: ECHO +RADIO_ID: 9990 +RX_FREQ: 449000000 +TX_FREQ: 444000000 +TX_POWER: 25 +COLORCODE: 1 +SLOTS: 1 +LATITUDE: 00.0000 +LONGITUDE: 000.0000 +HEIGHT: 0 +LOCATION: 9990 Parrot +DESCRIPTION: ECHO +URL: adn.systems +SOFTWARE_ID: 20170620 +PACKAGE_ID: MMDVM_ADN-Systems +GROUP_HANGTIME: 5 +OPTIONS: +USE_ACL: True +SUB_ACL: DENY:1 +TGID_TS1_ACL: PERMIT:ALL +TGID_TS2_ACL: PERMIT:ALL +ANNOUNCEMENT_LANGUAGE: en_GB diff --git a/loro.cfg b/config/parrot.cfg similarity index 92% rename from loro.cfg rename to config/parrot.cfg index 63eed1f..8506ed7 100644 --- a/loro.cfg +++ b/config/parrot.cfg @@ -47,7 +47,7 @@ TGID_TS1_ACL: PERMIT:ALL TGID_TS2_ACL: PERMIT:ALL GEN_STAT_BRIDGES: False ALLOW_NULL_PASSPHRASE: False -ANNOUNCEMENT_LANGUAGES: es_ES +ANNOUNCEMENT_LANGUAGES: en_GB SERVER_ID: 9990 DATA_GATEWAY: False VALIDATE_SERVER_IDS: False @@ -94,7 +94,7 @@ REPORT_CLIENTS: 127.0.0.1 LOG_FILE: /dev/null LOG_HANDLERS: null LOG_LEVEL: DEBUG -LOG_NAME: HBlink +LOG_NAME: ADN # DOWNLOAD AND IMPORT SUBSCRIBER, PEER and TGID ALIASES # Ok, not the TGID, there's no master list I know of to download @@ -104,18 +104,20 @@ LOG_NAME: HBlink # download again. Don't be an ass and change this to less than a few days. [ALIASES] TRY_DOWNLOAD: False -PATH: ./ +PATH: ./data PEER_FILE: peer_ids.json SUBSCRIBER_FILE: subscriber_ids.json TGID_FILE: talkgroup_ids.json -PEER_URL: https://www.radioid.net/static/rptrs.json -SUBSCRIBER_URL: https://www.radioid.net/static/users.json -TGID_URL: http://downloads.freedmr.uk/downloads/talkgroup_ids.json -LOCAL_SUBSCRIBER_FILE: local_subscriber_ids.json -STALE_DAYS: 7 +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 +LOCAL_SUBSCRIBER_FILE: local_subcriber_ids.json +STALE_DAYS: 1 SUB_MAP_FILE: -SERVER_ID_URL: http://downloads.freedmr.uk/downloads/FreeDMR_Hosts.csv +SERVER_ID_URL: https://adn.systems/files/server_ids.tsv SERVER_ID_FILE: server_ids.tsv +CHECKSUM_URL: https://adn.systems/files/file_checksums.json +CHECKSUM_FILE: file_checksums.json # OPENBRIDGE INSTANCES - DUPLICATE SECTION FOR MULTIPLE CONNECTIONS @@ -142,7 +144,7 @@ IP: PORT: 62044 NETWORK_ID: 1 PASSPHRASE: mypass -TARGET_IP: +TARGET_IP: TARGET_PORT: 62044 USE_ACL: True SUB_ACL: DENY:1 @@ -169,7 +171,7 @@ REPEAT: True MAX_PEERS: 1 EXPORT_AMBE: False IP: 127.0.0.1 -PORT: 54915 +PORT: 54916 PASSPHRASE: passw0rd GROUP_HANGTIME: 5 USE_ACL: True @@ -181,11 +183,10 @@ DEFAULT_UA_TIMER: 10 SINGLE_MODE: True VOICE_IDENT: False TS1_STATIC: -TS2_STATIC: +TS2_STATIC: DEFAULT_REFLECTOR: 0 GENERATOR: 1 -ANNOUNCEMENT_LANGUAGE:es_ES +ANNOUNCEMENT_LANGUAGE: en_GB ALLOW_UNREG_ID: True PROXY_CONTROL: False OVERRIDE_IDENT_TG: - diff --git a/data/.empty b/data/.empty new file mode 100644 index 0000000..e69de29 diff --git a/docker-configs/docker-compose.yml b/docker-configs/docker-compose.yml index b2fe633..9f07818 100644 --- a/docker-configs/docker-compose.yml +++ b/docker-configs/docker-compose.yml @@ -1,5 +1,4 @@ -############################################################################### -# Copyright (C) 2020 Simon Adlem, G7RZU +############################################################################### # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -14,24 +13,25 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# ############################################################################### version: '2.4' services: - freedmr: - container_name: freedmr + + adn-server: + container_name: adn-server cpu_shares: 1024 mem_reservation: 600m volumes: - - '/etc/freedmr/freedmr.cfg:/opt/freedmr/freedmr.cfg' - #Write JSON files outside of container - - '/etc/freedmr/json/:/opt/freedmr/json/' + - '/etc/ADN-Systems/ADN.cfg:/opt/ADN-DMR-Peer-Server/config/ADN.cfg' ports: - '62031:62031/udp' #Change the below to inlude ports used for your OBP(s) - '62041:62041/udp' - image: 'gitlab.hacknix.net:5050/hacknix/freedmr:latest' + - '8000:8000' + image: 'registry.gitlab.com/hp3icc/fdmr/adn-server-single:latest' restart: "unless-stopped" networks: app_net: @@ -48,45 +48,25 @@ services: - FDPROXY_DEBUG=0 #Override proxy external port #- FDPROXY_LISTENPORT=62031 - read_only: "true" - freedmrmonitor2: - container_name: freedmrmonitor2 - cpu_shares: 512 - depends_on: - - freedmr - image: 'gitlab.hacknix.net:5050/freedmr/freedmrmonitor2/freedmrmonitor2:monitor-latest' - restart: "unless-stopped" - networks: - app_net: - ipv4_address: 172.16.238.20 - read_only: "true" - logging: - driver: json-file - - freedmrmonpache: - container_name: freedmrmonapache + adn-dashboard: + container_name: adn-dashboard cpu_shares: 512 - depends_on: - - freedmrmonitor2 - #where to store TLS certificates - #and acme.sh files volumes: - - '/etc/freedmr/certs/:/opt/apachecerts/' - - '/etc/freedmr/acme.sh:/root/.acme.sh/' + - '/etc/freedmr/fdmr-mon.cfg:/opt/FDMR-Monitor/fdmr-mon.cfg' ports: - '80:80/tcp' - '443:443/tcp' - image: 'gitlab.hacknix.net:5050/freedmr/freedmrmonitor2/freedmrmonitor2:apache-latest' + - '9000:9000' + image: 'registry.gitlab.com/hp3icc/fdmr/adn-mon2-single:latest' restart: "unless-stopped" - environment: - #Set to 1 to enable TLS support + #Set USE_SSL = to True to enable TLS support #you'll need to actually generate the certtificates too #using these commands when the container is running: - #docker exec -it freedmrmonapache gencert.sh - #docker-compose restart freedmrmonapache + #docker exec -it adn-dashboard gencert.sh + #docker-compose restart adn-dashboard #This only needs to be done once - unless the files in the volumes above are deleted. @@ -94,10 +74,9 @@ services: #Note -the gencert.sh script only works when the webserver is available on the default port 80 #If it's on non-standard ports, you'll need to request the certificates manually. - - 'USE_TLS=1' networks: app_net: - ipv4_address: 172.16.238.30 + ipv4_address: 172.16.238.20 logging: driver: json-file diff --git a/docker-configs/docker-compose_install.sh b/docker-configs/docker-compose_install.sh index 8d22a75..0947c6a 100644 --- a/docker-configs/docker-compose_install.sh +++ b/docker-configs/docker-compose_install.sh @@ -138,7 +138,7 @@ DEFAULT_REFLECTOR: 0 ANNOUNCEMENT_LANGUAGE: en_GB GENERATOR: 100 ALLOW_UNREG_ID: False -PROXY_CONTROL: True +PROXY_CONTROL: False OVERRIDE_IDENT_TG: #Echo (Loro / Parrot) server diff --git a/docker-configs/freedmr.cfg b/docker-configs/freedmr.cfg index 50d2194..2a553d1 100644 --- a/docker-configs/freedmr.cfg +++ b/docker-configs/freedmr.cfg @@ -66,7 +66,7 @@ DEFAULT_REFLECTOR: 0 ANNOUNCEMENT_LANGUAGE: en_GB GENERATOR: 100 ALLOW_UNREG_ID: False -PROXY_CONTROL: True +PROXY_CONTROL: False OVERRIDE_IDENT_TG: #Echo (Loro / Parrot) server diff --git a/hblink-SAMPLE.cfg b/hblink-SAMPLE.cfg deleted file mode 100755 index a3b616c..0000000 --- a/hblink-SAMPLE.cfg +++ /dev/null @@ -1,241 +0,0 @@ -# PROGRAM-WIDE PARAMETERS GO HERE -# PATH - working path for files, leave it alone unless you NEED to change it -# PING_TIME - the interval that peers will ping the master, and re-try registraion -# - how often the Master maintenance loop runs -# MAX_MISSED - how many pings are missed before we give up and re-register -# - number of times the master maintenance loop runs before de-registering a peer -# -# ACLs: -# -# Access Control Lists are a very powerful tool for administering your system. -# But they consume packet processing time. Disable them if you are not using them. -# But be aware that, as of now, the configuration stanzas still need the ACL -# sections configured even if you're not using them. -# -# REGISTRATION ACLS ARE ALWAYS USED, ONLY SUBSCRIBER AND TGID MAY BE DISABLED!!! -# -# The 'action' May be PERMIT|DENY -# Each entry may be a single radio id, or a hypenated range (e.g. 1-2999) -# Format: -# ACL = 'action:id|start-end|,id|start-end,....' -# --for example-- -# SUB_ACL: DENY:1,1000-2000,4500-60000,17 -# -# ACL Types: -# REG_ACL: peer radio IDs for registration (only used on HBP master systems) -# SUB_ACL: subscriber IDs for end-users -# TGID_TS1_ACL: destination talkgroup IDs on Timeslot 1 -# TGID_TS2_ACL: destination talkgroup IDs on Timeslot 2 -# -# ACLs may be repeated for individual systems if needed for granularity -# Global ACLs will be processed BEFORE the system level ACLs -# Packets will be matched against all ACLs, GLOBAL first. If a packet 'passes' -# All elements, processing continues. Packets are discarded at the first -# negative match, or 'reject' from an ACL element. -# -# If you do not wish to use ACLs, set them to 'PERMIT:ALL' -# TGID_TS1_ACL in the global stanza is used for OPENBRIDGE systems, since all -# traffic is passed as TS 1 between OpenBridges -[GLOBAL] -PATH: ./ -PING_TIME: 5 -MAX_MISSED: 3 -USE_ACL: True -REG_ACL: PERMIT:ALL -SUB_ACL: DENY:1 -TGID_TS1_ACL: PERMIT:ALL -TGID_TS2_ACL: PERMIT:ALL - - -# NOT YET WORKING: NETWORK REPORTING CONFIGURATION -# Enabling "REPORT" will configure a socket-based reporting -# system that will send the configuration and other items -# to a another process (local or remote) that may process -# the information for some useful purpose, like a web dashboard. -# -# REPORT - True to enable, False to disable -# REPORT_INTERVAL - Seconds between reports -# REPORT_PORT - TCP port to listen on if "REPORT_NETWORKS" = NETWORK -# REPORT_CLIENTS - comma separated list of IPs you will allow clients -# to connect on. Entering a * will allow all. -# -# ****FOR NOW MUST BE TRUE - USE THE LOOPBACK IF YOU DON'T USE THIS!!!**** -[REPORTS] -REPORT: True -REPORT_INTERVAL: 60 -REPORT_PORT: 4321 -REPORT_CLIENTS: 127.0.0.1 - - -# SYSTEM LOGGER CONFIGURAITON -# This allows the logger to be configured without chaning the individual -# python logger stuff. LOG_FILE should be a complete path/filename for *your* -# system -- use /dev/null for non-file handlers. -# LOG_HANDLERS may be any of the following, please, no spaces in the -# list if you use several: -# null -# console -# console-timed -# file -# file-timed -# syslog -# LOG_LEVEL may be any of the standard syslog logging levels, though -# as of now, DEBUG, INFO, WARNING and CRITICAL are the only ones -# used. -# -[LOGGER] -LOG_FILE: /tmp/hblink.log -LOG_HANDLERS: console-timed -LOG_LEVEL: DEBUG -LOG_NAME: HBlink - -# DOWNLOAD AND IMPORT SUBSCRIBER, PEER and TGID ALIASES -# Ok, not the TGID, there's no master list I know of to download -# This is intended as a facility for other applcations built on top of -# HBlink to use, and will NOT be used in HBlink directly. -# STALE_DAYS is the number of days since the last download before we -# download again. Don't be an ass and change this to less than a few days. -[ALIASES] -TRY_DOWNLOAD: True -PATH: ./ -PEER_FILE: peer_ids.json -SUBSCRIBER_FILE: subscriber_ids.json -TGID_FILE: talkgroup_ids.json -PEER_URL: https://www.radioid.net/static/rptrs.json -SUBSCRIBER_URL: https://www.radioid.net/static/users.json -STALE_DAYS: 7 - -# OPENBRIDGE INSTANCES - DUPLICATE SECTION FOR MULTIPLE CONNECTIONS -# OpenBridge is a protocol originall created by DMR+ for connection between an -# IPSC2 server and Brandmeister. It has been implemented here at the suggestion -# of the Brandmeister team as a way to legitimately connect HBlink to the -# Brandemiester network. -# It is recommended to name the system the ID of the Brandmeister server that -# it connects to, but is not necessary. TARGET_IP and TARGET_PORT are of the -# Brandmeister or IPSC2 server you are connecting to. PASSPHRASE is the password -# that must be agreed upon between you and the operator of the server you are -# connecting to. NETWORK_ID is a number in the format of a DMR Radio ID that -# will be sent to the other server to identify this connection. -# other parameters follow the other system types. -# -# ACLs: -# OpenBridge does not 'register', so registration ACL is meaningless. -# OpenBridge passes all traffic on TS1, so there is only 1 TGID ACL. -# Otherwise ACLs work as described in the global stanza -[OBP-1] -MODE: OPENBRIDGE -ENABLED: True -IP: -PORT: 62035 -NETWORK_ID: 3129100 -PASSPHRASE: password -TARGET_IP: 1.2.3.4 -TARGET_PORT: 62035 -USE_ACL: True -SUB_ACL: DENY:1 -TGID_ACL: PERMIT:ALL - -# MASTER INSTANCES - DUPLICATE SECTION FOR MULTIPLE MASTERS -# HomeBrew Protocol Master instances go here. -# IP may be left blank if there's one interface on your system. -# Port should be the port you want this master to listen on. It must be unique -# and unused by anything else. -# Repeat - if True, the master repeats traffic to peers, False, it does nothing. -# -# MAX_PEERS -- maximun number of peers that may be connect to this master -# at any given time. This is very handy if you're allowing hotspots to -# connect, or using a limited computer like a Raspberry Pi. -# -# ACLs: -# See comments in the GLOBAL stanza -[MASTER-1] -MODE: MASTER -ENABLED: True -REPEAT: True -MAX_PEERS: 10 -EXPORT_AMBE: False -IP: -PORT: 54000 -PASSPHRASE: s3cr37w0rd -GROUP_HANGTIME: 5 -USE_ACL: True -REG_ACL: DENY:1 -SUB_ACL: DENY:1 -TGID_TS1_ACL: PERMIT:ALL -TGID_TS2_ACL: PERMIT:ALL - -# PEER INSTANCES - DUPLICATE SECTION FOR MULTIPLE PEERS -# There are a LOT of errors in the HB Protocol specifications on this one! -# MOST of these items are just strings and will be properly dealt with by the program -# The TX & RX Frequencies are 9-digit numbers, and are the frequency in Hz. -# Latitude is an 8-digit unsigned floating point number. -# Longitude is a 9-digit signed floating point number. -# Height is in meters -# Setting Loose to True relaxes the validation on packets received from the master. -# This will allow HBlink to connect to a non-compliant system such as XLXD, DMR+ etc. -# -# ACLs: -# See comments in the GLOBAL stanza -[REPEATER-1] -MODE: PEER -ENABLED: True -LOOSE: False -EXPORT_AMBE: False -IP: -PORT: 54001 -MASTER_IP: 172.16.1.1 -MASTER_PORT: 54000 -PASSPHRASE: homebrew -CALLSIGN: W1ABC -RADIO_ID: 312000 -RX_FREQ: 449000000 -TX_FREQ: 444000000 -TX_POWER: 25 -COLORCODE: 1 -SLOTS: 1 -LATITUDE: 38.0000 -LONGITUDE: -095.0000 -HEIGHT: 75 -LOCATION: Anywhere, USA -DESCRIPTION: This is a cool repeater -URL: www.w1abc.org -SOFTWARE_ID: 20170620 -PACKAGE_ID: MMDVM_HBlink -GROUP_HANGTIME: 5 -OPTIONS: -USE_ACL: True -SUB_ACL: DENY:1 -TGID_TS1_ACL: PERMIT:ALL -TGID_TS2_ACL: PERMIT:ALL - -[XLX-1] -MODE: XLXPEER -ENABLED: True -LOOSE: True -EXPORT_AMBE: False -IP: -PORT: 54002 -MASTER_IP: 172.16.1.1 -MASTER_PORT: 62030 -PASSPHRASE: passw0rd -CALLSIGN: W1ABC -RADIO_ID: 312000 -RX_FREQ: 449000000 -TX_FREQ: 444000000 -TX_POWER: 25 -COLORCODE: 1 -SLOTS: 1 -LATITUDE: 38.0000 -LONGITUDE: -095.0000 -HEIGHT: 75 -LOCATION: Anywhere, USA -DESCRIPTION: This is a cool repeater -URL: www.w1abc.org -SOFTWARE_ID: 20170620 -PACKAGE_ID: MMDVM_HBlink -GROUP_HANGTIME: 5 -XLXMODULE: 4004 -USE_ACL: True -SUB_ACL: DENY:1 -TGID_TS1_ACL: PERMIT:ALL -TGID_TS2_ACL: PERMIT:ALL diff --git a/hotspot_proxy_self_service.py b/hotspot_proxy_self_service.py new file mode 100644 index 0000000..86d971d --- /dev/null +++ b/hotspot_proxy_self_service.py @@ -0,0 +1,684 @@ +#!/usr/bin/env python3 +############################################################################### +# Copyright (C) 2020 Simon Adlem, G7RZU +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +############################################################################### +from hashlib import pbkdf2_hmac +import random +import ipaddress +import os +from datetime import datetime +from time import time + +from twisted.internet.protocol import DatagramProtocol +from twisted.internet.defer import inlineCallbacks +from twisted.internet import reactor, task +from setproctitle import setproctitle +from dmr_utils3.utils import int_id +import Pyro5.api +from proxy_db import ProxyDB + +# Does anybody read this stuff? There's a PEP somewhere that says I should do this. +__author__ = "Simon Adlem - G7RZU" +__verion__ = "23.10.14" +__copyright__ = "Copyright (c) Simon Adlem, G7RZU 2020,2021,2022,2023" +__credits__ = "Jon Lee, G4TSN; Norman Williams, M6NBP; Christian, OA4DOA" +__license__ = "GNU GPLv3" +__maintainer__ = "Simon Adlem G7RZU" +__email__ = "simon@gb7fr.org.uk" + + +def IsIPv4Address(ip): + try: + ipaddress.IPv4Address(ip) + return True + except ValueError as errorCode: + pass + return False + + +def IsIPv6Address(ip): + try: + ipaddress.IPv6Address(ip) + return True + except ValueError as errorCode: + pass + + +class privHelper: + def __init__(self): + self._netfilterURI = ( + "PYRO:netfilterControl@./u:/run/priv_control/priv_control.unixsocket" + ) + self._conntrackURI = ( + "PYRO:conntrackControl@./u:/run/priv_control/priv_control.unixsocket" + ) + + def addBL(self, dport, ip): + try: + with Pyro5.api.Proxy(self._netfilterURI) as nf: + nf.blocklistAdd(dport, ip) + except Exception as e: + print("(PrivError) {}".format(e)) + + def delBL(self, dport, ip): + try: + with Pyro5.api.Proxy(self._netfilterURI) as nf: + nf.blocklistDel(dport, ip) + except Exception as e: + print("(PrivError) {}".format(e)) + + def blocklistFlush(self): + try: + with Pyro5.api.Proxy(self._netfilterURI) as nf: + nf.blocklistFlush() + except Exception as e: + print("(PrivError) {}".format(e)) + + def flushCT(self): + try: + with Pyro5.api.Proxy(self._conntrackURI) as ct: + ct.flushUDPTarget(62031) + except Exception as e: + print("(PrivError) {}".format(e)) + + +class Proxy(DatagramProtocol): + + def __init__( + self, + Master, + ListenPort, + connTrack, + peerTrack, + blackList, + IPBlackList, + Timeout, + Debug, + ClientInfo, + DestportStart, + DestPortEnd, + privHelper, + rptlTrack, + db_proxy, + selfservice, + ): + self.master = Master + self.ListenPort = ListenPort + self.connTrack = connTrack + self.peerTrack = peerTrack + self.timeout = Timeout + self.debug = Debug + self.clientinfo = ClientInfo + self.blackList = blackList + self.IPBlackList = IPBlackList + self.destPortStart = DestportStart + self.destPortEnd = DestPortEnd + self.numPorts = DestPortEnd - DestportStart + self.privHelper = privHelper + self.rptlTrack = rptlTrack + self.db_proxy = db_proxy + self.selfserv = selfservice + + def reaper(self, _peer_id): + if self.debug: + print("dead", _peer_id) + if self.clientinfo and _peer_id != b"\xff\xff\xff\xff": + print( + f"{datetime.now().replace(microsecond=0)} Client: ID:{str(int_id(_peer_id)).rjust(9)} IP:{self.peerTrack[_peer_id]['shost'].rjust(15)} Port:{self.peerTrack[_peer_id]['sport']} Removed." + ) + self.transport.write( + b"RPTCL" + _peer_id, (self.master, self.peerTrack[_peer_id]["dport"]) + ) + # Tell client we have closed the session - 3 times, in case they are on a lossy network + self.transport.write( + b"MSTCL", + (self.peerTrack[_peer_id]["shost"], self.peerTrack[_peer_id]["sport"]), + ) + self.transport.write( + b"MSTCL", + (self.peerTrack[_peer_id]["shost"], self.peerTrack[_peer_id]["sport"]), + ) + self.transport.write( + b"MSTCL", + (self.peerTrack[_peer_id]["shost"], self.peerTrack[_peer_id]["sport"]), + ) + self.connTrack[self.peerTrack[_peer_id]["dport"]] = False + if self.selfserv: + self.db_proxy.updt_tbl("log_out", _peer_id) + del self.peerTrack[_peer_id] + + def datagramReceived(self, data, addr): + + # HomeBrew Protocol Commands + DMRD = b"DMRD" + DMRA = b"DMRA" + MSTCL = b"MSTCL" + MSTNAK = b"MSTNAK" + MSTPONG = b"MSTPONG" + MSTN = b"MSTN" + MSTP = b"MSTP" + MSTC = b"MSTC" + RPTL = b"RPTL" + RPTPING = b"RPTPING" + RPTCL = b"RPTCL" + RPTL = b"RPTL" + RPTACK = b"RPTACK" + RPTK = b"RPTK" + RPTC = b"RPTC" + RPTP = b"RPTP" + RPTA = b"RPTA" + RPTO = b"RPTO" + + # Proxy control commands + PRBL = b"PRBL" + + # Proxy info commands + PRIN = b"PRIN" + + _peer_id = False + + host, port = addr + + nowtime = time() + + Debug = self.debug + + if host in self.IPBlackList: + return + + # If the packet comes from the master + if host == self.master: + _command = data[:4] + + if _command == PRBL: + _peer_id = data[4:8] + _bltime = data[8:].decode("UTF-8") + _bltime = float(_bltime) + try: + self.IPBlackList[self.peerTrack[_peer_id]["shost"]] = _bltime + except KeyError: + return + if self.clientinfo: + print( + "Add to blacklist: host {}. Expire time {}".format( + self.peerTrack[_peer_id]["shost"], _bltime + ) + ) + if self.privHelper: + print( + "Ask priv_helper to add to iptables: host {}, port {}.".format( + self.peerTrack[_peer_id]["shost"], self.ListenPort + ) + ) + reactor.callInThread( + self.privHelper.addBL, + self.ListenPort, + self.peerTrack[_peer_id]["shost"], + ) + return + + if _command == DMRD: + _peer_id = data[11:15] + elif _command == RPTA: + if data[6:10] in self.peerTrack: + _peer_id = data[6:10] + else: + _peer_id = self.connTrack[port] + elif _command == MSTN: + _peer_id = data[6:10] + elif _command == MSTP: + _peer_id = data[7:11] + elif _command == MSTC: + _peer_id = data[5:9] + + if self.debug: + print(data) + if _peer_id in self.peerTrack: + self.transport.write( + data, + ( + self.peerTrack[_peer_id]["shost"], + self.peerTrack[_peer_id]["sport"], + ), + ) + # Remove the client after send a MSTN or MSTC packet + if _command in (MSTN, MSTC): + # Give time to the client for a reply to prevent port reassignment + self.peerTrack[_peer_id]["timer"].reset(15) + + return + + else: + _command = data[:4] + + if _command == DMRD: # DMRData -- encapsulated DMR data frame + _peer_id = data[11:15] + elif _command == DMRA: # DMRAlias -- Talker Alias information + _peer_id = data[4:8] + elif _command == RPTL: # RPTLogin -- a repeater wants to login + _peer_id = data[4:8] + + # if we have seen more than 50 RPTL packets from this IP since the RPTL tracking table was reset (every 60 secs) + # blacklist IP for 10 minutes + if host not in self.rptlTrack: + self.rptlTrack[host] = 1 + else: + self.rptlTrack[host] += 1 + + if self.rptlTrack[host] > 50: + print("(RPTL) exceeded max: {}".format(self.rptlTrack[host])) + _bltime = nowtime + 600 + self.IPBlackList[host] = _bltime + self.rptlTrack.pop(host) + + if self.clientinfo: + print( + "(RPTL) Add to blacklist: host {}. Expire time {}".format( + host, _bltime + ) + ) + if self.privHelper: + print( + "(RPTL) Ask priv_helper to add to iptables: host {}, port {}.".format( + host, self.ListenPort + ) + ) + reactor.callInThread( + self.privHelper.addBL, self.ListenPort, host + ) + return + + elif _command == RPTK: # Repeater has answered our login challenge + _peer_id = data[4:8] + elif ( + _command == RPTC + ): # Repeater is sending it's configuraiton OR disconnecting + if data[:5] == RPTCL: # Disconnect command + _peer_id = data[5:9] + else: + _peer_id = data[4:8] # Configure Command + if self.selfserv and _peer_id in self.peerTrack: + mode = data[97:98].decode() + callsign = data[8:16].rstrip().decode() + self.db_proxy.ins_conf( + int_id(_peer_id), _peer_id, callsign, addr[0], mode + ) + # Self Service options will be send 10 sec. after login + self.peerTrack[_peer_id]["opt_timer"] = reactor.callLater( + 10, self.login_opt, _peer_id + ) + + elif _command == RPTO: # options + _peer_id = data[4:8] + if self.selfserv and _peer_id in self.peerTrack: + # Store Self Service password in database + if data[8:].upper().startswith(b"PASS="): + _psswd = data[13:] + if len(_psswd) >= 6: + dk = pbkdf2_hmac("sha256", _psswd, b"FreeDMR", 2000).hex() + self.db_proxy.updt_tbl("psswd", _peer_id, psswd=dk) + self.transport.write(b"".join([RPTACK, _peer_id]), addr) + print(f"Password stored for: {int_id(_peer_id)}") + return + self.db_proxy.updt_tbl("opt_rcvd", _peer_id) + # Options send by peer overrides Self Service options + if self.peerTrack[_peer_id]["opt_timer"].active(): + self.peerTrack[_peer_id]["opt_timer"].cancel() + print(f"Options received from: {int_id(_peer_id)}") + + elif _command == RPTP: # RPTPing -- peer is pinging us + _peer_id = data[7:11] + else: + return + + if _peer_id in self.peerTrack: + _dport = self.peerTrack[_peer_id]["dport"] + self.peerTrack[_peer_id]["sport"] = port + self.peerTrack[_peer_id]["shost"] = host + self.transport.write(data, (self.master, _dport)) + self.peerTrack[_peer_id]["timer"].reset(self.timeout) + if self.debug: + print(data) + return + + else: + if int_id(_peer_id) in self.blackList: + return + # Make a list with the available ports + _ports_avail = [ + port for port in self.connTrack if not self.connTrack[port] + ] + if _ports_avail: + _dport = random.choice(_ports_avail) + else: + return + self.connTrack[_dport] = _peer_id + self.peerTrack[_peer_id] = {} + self.peerTrack[_peer_id]["dport"] = _dport + self.peerTrack[_peer_id]["sport"] = port + self.peerTrack[_peer_id]["shost"] = host + self.peerTrack[_peer_id]["timer"] = reactor.callLater( + self.timeout, self.reaper, _peer_id + ) + self.transport.write(data, (self.master, _dport)) + pripacket = b"".join( + [b"PRIN", host.encode("UTF-8"), b":", str(port).encode("UTF-8")] + ) + # Send IP and Port info to server + self.transport.write(pripacket, (self.master, _dport)) + + if self.clientinfo and _peer_id != b"\xff\xff\xff\xff": + print( + f"{datetime.now().replace(microsecond=0)} New client: ID:{str(int_id(_peer_id)).rjust(9)} IP:{host.rjust(15)} Port:{port}, assigned to port:{_dport}." + ) + if self.debug: + print(data) + return + + @inlineCallbacks + def login_opt(self, _peer_id): + try: + res = yield db_proxy.slct_opt(_peer_id) + options = res[0][0] + if options: + bytes_pkt = b"".join((b"RPTO", _peer_id, options.encode())) + self.transport.write( + bytes_pkt, (self.master, self.peerTrack[_peer_id]["dport"]) + ) + print(f"Options sent at login for: {int_id(_peer_id)}, opt: {options}") + + except Exception as err: + print(f"login_opt error: {err}") + + @inlineCallbacks + def send_opts(self): + try: + results = yield db_proxy.slct_db() + for item in results: + _peer_id, options = item + if _peer_id not in self.peerTrack or not options: + continue + self.db_proxy.updt_tbl("rst_mod", _peer_id) + bytes_pkt = b"".join((b"RPTO", _peer_id, options.encode())) + self.transport.write( + bytes_pkt, (self.master, self.peerTrack[_peer_id]["dport"]) + ) + print(f"Options update sent for: {int_id(_peer_id)}") + + except Exception as err: + print(f"send_opts error: {err}") + + def lst_seen(self): + # Update last seen + dmrid_list = [(ite,) for ite in self.peerTrack] + if dmrid_list: + self.db_proxy.updt_lstseen(dmrid_list) + + +if __name__ == "__main__": + + import signal + import configparser + import argparse + import sys + import json + import stat + import functools + + print = functools.partial(print, flush=True) + + # Set process title early + setproctitle(__file__) + + # Change the current directory to the location of the application + os.chdir(os.path.dirname(os.path.realpath(sys.argv[0]))) + + # CLI argument parser - handles picking up the config file from the command line, and sending a "help" message + parser = argparse.ArgumentParser() + parser.add_argument( + "-c", + "--config", + action="store", + dest="CONFIG_FILE", + help="/full/path/to/config.file (usually freedmr.cfg)", + ) + cli_args = parser.parse_args() + + # Ensure we have a path for the config file, if one wasn't specified, then use the execution directory + if not cli_args.CONFIG_FILE: + cli_args.CONFIG_FILE = ( + os.path.dirname(os.path.abspath(__file__)) + "/config/adn.cfg" + ) + + _config_file = cli_args.CONFIG_FILE + + config = configparser.ConfigParser() + + if not config.read(_config_file): + print( + "Configuration file '" + + _config_file + + "' is not a valid configuration file!" + ) + + try: + + Master = config.get("PROXY", "Master") + ListenPort = config.getint("PROXY", "ListenPort") + ListenIP = config.get("PROXY", "ListenIP") + DestportStart = config.getint("PROXY", "DestportStart") + DestPortEnd = config.getint("PROXY", "DestPortEnd") + Timeout = config.getint("PROXY", "Timeout") + Stats = config.getboolean("PROXY", "Stats") + Debug = config.getboolean("PROXY", "Debug") + ClientInfo = config.getboolean("PROXY", "ClientInfo") + BlackList = json.loads(config.get("PROXY", "BlackList")) + IPBlackList = json.loads(config.get("PROXY", "IPBlackList")) + # Self Service + use_selfservice = config.getboolean("SELF SERVICE", "use_selfservice") + db_server = config.get("SELF SERVICE", "server") + db_username = config.get("SELF SERVICE", "username") + db_password = config.get("SELF SERVICE", "password") + db_name = config.get("SELF SERVICE", "db_name") + db_port = config.getint("SELF SERVICE", "port") + + except configparser.Error as err: + print("Error processing configuration file -- {}".format(err)) + + print("Using default config") + # *** CONFIG HERE *** + + Master = "127.0.0.1" + ListenPort = 62031 + #'' = all IPv4, '::' = all IPv4 and IPv6 (Dual Stack) + ListenIP = "" + DestportStart = 56400 + DestPortEnd = 56500 + Timeout = 30 + Stats = False + Debug = False + ClientInfo = False + BlackList = [1234567] + # e.g. {10.0.0.1: 0, 10.0.0.2: 0} + IPBlackList = {} + + # Self Service database configuration + use_selfservice = True + db_server = "localhost" + db_username = "root" + db_password = "" + db_name = "test" + db_port = 3306 + + # ******************* + + CONNTRACK = {} + PEERTRACK = {} + RPTLTRACK = {} + PRIV_HELPER = None + + # Set up the signal handler + def sig_handler(_signal, _frame): + print( + "(GLOBAL) SHUTDOWN: PROXY IS TERMINATING WITH SIGNAL {}".format( + str(_signal) + ) + ) + reactor.stop() + + # Install signal handlers + signal.signal(signal.SIGTERM, sig_handler) + signal.signal(signal.SIGINT, sig_handler) + + # readState() + + # If IPv6 is enabled by enivornment variable... + if ( + ListenIP == "" + and "FDPROXY_IPV6" in os.environ + and bool(os.environ["FDPROXY_IPV6"]) + ): + ListenIP = "::" + + # Override static config from Environment + if "FDPROXY_STATS" in os.environ: + Stats = bool(os.environ["FDPROXY_STATS"]) + # if 'FDPROXY_DEBUG' in os.environ: + # Debug = bool(os.environ['FDPROXY_DEBUG']) + if "FDPROXY_CLIENTINFO" in os.environ: + ClientInfo = bool(os.environ["FDPROXY_CLIENTINFO"]) + if "FDPROXY_LISTENPORT" in os.environ: + ListenPort = int(os.environ["FDPROXY_LISTENPORT"]) + + unixSocket = "/run/priv_control/priv_control.unixsocket" + + if os.path.exists(unixSocket) and stat.S_ISSOCK(os.stat(unixSocket).st_mode): + print("(PRIV) Found UNIX socket. Enabling priv helper") + PRIV_HELPER = privHelper() + print("(PRIV) flush conntrack") + PRIV_HELPER.flushCT() + print("(PRIV) flush blocklist") + PRIV_HELPER.blocklistFlush() + + for port in range(DestportStart, DestPortEnd + 1, 1): + CONNTRACK[port] = False + + # If we are listening IPv6 and Master is an IPv4 IPv4Address + # IPv6ify the address. + if ListenIP == "::" and IsIPv4Address(Master): + Master = "::ffff:" + Master + + if use_selfservice: + # Create an instance of db_proxy and them pass it to the proxy + db_proxy = ProxyDB(db_server, db_username, db_password, db_name, db_port) + db_proxy.test_db(reactor) + else: + db_proxy = None + + srv_proxy = Proxy( + Master, + ListenPort, + CONNTRACK, + PEERTRACK, + BlackList, + IPBlackList, + Timeout, + Debug, + ClientInfo, + DestportStart, + DestPortEnd, + PRIV_HELPER, + RPTLTRACK, + db_proxy, + use_selfservice, + ) + + reactor.listenUDP(ListenPort, srv_proxy, interface=ListenIP) + + def loopingErrHandle(failure): + print( + "(GLOBAL) STOPPING REACTOR TO AVOID MEMORY LEAK: Unhandled error innowtimed loop.\n {}".format( + failure + ) + ) + reactor.stop() + + if use_selfservice: + # Options loop + opts_loop = task.LoopingCall(srv_proxy.send_opts) + opts_loop.start(10).addErrback(loopingErrHandle) + + # Clean table every hour + cl_tbl = task.LoopingCall(db_proxy.clean_tbl) + cl_tbl.start(3600).addErrback(loopingErrHandle) + + # Update last seen loop + ls_loop = task.LoopingCall(srv_proxy.lst_seen) + ls_loop.start(120).addErrback(loopingErrHandle) + + def stats(): + count = 0 + nowtime = time() + for port in CONNTRACK: + if CONNTRACK[port]: + count = count + 1 + + totalPorts = DestPortEnd - DestportStart + freePorts = totalPorts - count + + print( + "{} ports out of {} in use ({} free)".format(count, totalPorts, freePorts) + ) + + def blackListTrimmer(): + _timenow = time() + _dellist = [] + for entry in IPBlackList: + deletetime = IPBlackList[entry] + if deletetime and deletetime < _timenow: + _dellist.append(entry) + + for delete in _dellist: + IPBlackList.pop(delete) + if ClientInfo: + print("Remove dynamic blacklist entry for {}".format(delete)) + if PRIV_HELPER: + print( + "Ask priv helper to remove blacklist entry for {} from iptables".format( + delete + ) + ) + reactor.callInThread(PRIV_HELPER.delBL, ListenPort, delete) + + def rptlTrimmer(): + RPTLTRACK.clear() + print("Purge RPTL table") + + if Stats == True: + stats_task = task.LoopingCall(stats) + statsa = stats_task.start(30) + statsa.addErrback(loopingErrHandle) + + blacklist_task = task.LoopingCall(blackListTrimmer) + blacklista = blacklist_task.start(15) + blacklista.addErrback(loopingErrHandle) + + rptlTrimmer_task = task.LoopingCall(rptlTrimmer) + rptlTrimmera = rptlTrimmer_task.start(60) + rptlTrimmera.addErrback(loopingErrHandle) + + reactor.run() diff --git a/hotspot_proxy_v2.py b/hotspot_proxy_v2.py index 03ce5c0..0878cfc 100644 --- a/hotspot_proxy_v2.py +++ b/hotspot_proxy_v2.py @@ -196,10 +196,10 @@ class Proxy(DatagramProtocol): if _command in (MSTN,MSTC): # Give time to the client for a reply to prevent port reassignment self.peerTrack[_peer_id]['timer'].reset(15) - + return - + else: _command = data[:4] @@ -307,7 +307,7 @@ if __name__ == '__main__': # Ensure we have a path for the config file, if one wasn't specified, then use the execution directory if not cli_args.CONFIG_FILE: - cli_args.CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/freedmr.cfg' + cli_args.CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/config/adn.cfg' _config_file = cli_args.CONFIG_FILE @@ -340,8 +340,8 @@ if __name__ == '__main__': ListenPort = 62031 #'' = all IPv4, '::' = all IPv4 and IPv6 (Dual Stack) ListenIP = '' - DestportStart = 54000 - DestPortEnd = 54100 + DestportStart = 56400 + DestPortEnd = 56500 Timeout = 30 Stats = False Debug = False diff --git a/playback_file.cfg b/playback_file.cfg deleted file mode 100644 index 89f269d..0000000 --- a/playback_file.cfg +++ /dev/null @@ -1,233 +0,0 @@ -# PROGRAM-WIDE PARAMETERS GO HERE -# PATH - working path for files, leave it alone unless you NEED to change it -# PING_TIME - the interval that peers will ping the master, and re-try registraion -# - how often the Master maintenance loop runs -# MAX_MISSED - how many pings are missed before we give up and re-register -# - number of times the master maintenance loop runs before de-registering a peer -# -# ACLs: -# -# Access Control Lists are a very powerful tool for administering your system. -# But they consume packet processing time. Disable them if you are not using them. -# But be aware that, as of now, the configuration stanzas still need the ACL -# sections configured even if you're not using them. -# -# REGISTRATION ACLS ARE ALWAYS USED, ONLY SUBSCRIBER AND TGID MAY BE DISABLED!!! -# -# The 'action' May be PERMIT|DENY -# Each entry may be a single radio id, or a hypenated range (e.g. 1-2999) -# Format: -# ACL = 'action:id|start-end|,id|start-end,....' -# --for example-- -# SUB_ACL: DENY:1,1000-2000,4500-60000,17 -# -# ACL Types: -# REG_ACL: peer radio IDs for registration (only used on HBP master systems) -# SUB_ACL: subscriber IDs for end-users -# TGID_TS1_ACL: destination talkgroup IDs on Timeslot 1 -# TGID_TS2_ACL: destination talkgroup IDs on Timeslot 2 -# -# ACLs may be repeated for individual systems if needed for granularity -# Global ACLs will be processed BEFORE the system level ACLs -# Packets will be matched against all ACLs, GLOBAL first. If a packet 'passes' -# All elements, processing continues. Packets are discarded at the first -# negative match, or 'reject' from an ACL element. -# -# If you do not wish to use ACLs, set them to 'PERMIT:ALL' -# TGID_TS1_ACL in the global stanza is used for OPENBRIDGE systems, since all -# traffic is passed as TS 1 between OpenBridges -[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: False -ALLOW_NULL_PASSPHRASE: False -ANNOUNCEMENT_LANGUAGES: es_ES -SERVER_ID: 9990 -DATA_GATEWAY: False -VALIDATE_SERVER_IDS: False - - - -# NOT YET WORKING: NETWORK REPORTING CONFIGURATION -# Enabling "REPORT" will configure a socket-based reporting -# system that will send the configuration and other items -# to a another process (local or remote) that may process -# the information for some useful purpose, like a web dashboard. -# -# REPORT - True to enable, False to disable -# REPORT_INTERVAL - Seconds between reports -# REPORT_PORT - TCP port to listen on if "REPORT_NETWORKS" = NETWORK -# REPORT_CLIENTS - comma separated list of IPs you will allow clients -# to connect on. Entering a * will allow all. -# -# ****FOR NOW MUST BE TRUE - USE THE LOOPBACK IF YOU DON'T USE THIS!!!**** -[REPORTS] -REPORT: False -REPORT_INTERVAL: 60 -REPORT_PORT: 4821 -REPORT_CLIENTS: 127.0.0.1 - - -# SYSTEM LOGGER CONFIGURAITON -# This allows the logger to be configured without chaning the individual -# python logger stuff. LOG_FILE should be a complete path/filename for *your* -# system -- use /dev/null for non-file handlers. -# LOG_HANDLERS may be any of the following, please, no spaces in the -# list if you use several: -# null -# console -# console-timed -# file -# file-timed -# syslog -# LOG_LEVEL may be any of the standard syslog logging levels, though -# as of now, DEBUG, INFO, WARNING and CRITICAL are the only ones -# used. -# -[LOGGER] -LOG_FILE: /dev/null -LOG_HANDLERS: console-timed -LOG_LEVEL: DEBUG -LOG_NAME: HBlink - -# DOWNLOAD AND IMPORT SUBSCRIBER, PEER and TGID ALIASES -# Ok, not the TGID, there's no master list I know of to download -# This is intended as a facility for other applcations built on top of -# HBlink to use, and will NOT be used in HBlink directly. -# STALE_DAYS is the number of days since the last download before we -# download again. Don't be an ass and change this to less than a few days. -[ALIASES] -TRY_DOWNLOAD: False -PATH: ./ -PEER_FILE: peer_ids.json -SUBSCRIBER_FILE: subscriber_ids.json -TGID_FILE: talkgroup_ids.json -PEER_URL: https://www.radioid.net/static/rptrs.json -SUBSCRIBER_URL: https://www.radioid.net/static/users.json -TGID_URL: http://downloads.freedmr.uk/downloads/talkgroup_ids.json -LOCAL_SUBSCRIBER_FILE: local_subscriber_ids.json -STALE_DAYS: 7 -SUB_MAP_FILE: -SERVER_ID_FILE: -SERVER_ID_URL: - -#Read further repeater configs from MySQL -[MYSQL] -USE_MYSQL: False -USER: hblink -PASS: mypassword -DB: hblink -SERVER: 127.0.0.1 -PORT: 3306 -TABLE: repeaters - -# OPENBRIDGE INSTANCES - DUPLICATE SECTION FOR MULTIPLE CONNECTIONS -# OpenBridge is a protocol originall created by DMR+ for connection between an -# IPSC2 server and Brandmeister. It has been implemented here at the suggestion -# of the Brandmeister team as a way to legitimately connect HBlink to the -# Brandemiester network. -# It is recommended to name the system the ID of the Brandmeister server that -# it connects to, but is not necessary. TARGET_IP and TARGET_PORT are of the -# Brandmeister or IPSC2 server you are connecting to. PASSPHRASE is the password -# that must be agreed upon between you and the operator of the server you are -# connecting to. NETWORK_ID is a number in the format of a DMR Radio ID that -# will be sent to the other server to identify this connection. -# other parameters follow the other system types. -# -# ACLs: -# OpenBridge does not 'register', so registration ACL is meaningless. -# OpenBridge passes all traffic on TS1, so there is only 1 TGID ACL. -# Otherwise ACLs work as described in the global stanza -[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: PERMIT:ALL -RELAX_CHECKS: False - -# MASTER INSTANCES - DUPLICATE SECTION FOR MULTIPLE MASTERS -# HomeBrew Protocol Master instances go here. -# IP may be left blank if there's one interface on your system. -# Port should be the port you want this master to listen on. It must be unique -# and unused by anything else. -# Repeat - if True, the master repeats traffic to peers, False, it does nothing. -# -# MAX_PEERS -- maximun number of peers that may be connect to this master -# at any given time. This is very handy if you're allowing hotspots to -# connect, or using a limited computer like a Raspberry Pi. -# -# ACLs: -# See comments in the GLOBAL stanza -[PARROT] -MODE: MASTER -ENABLED: False -REPEAT: True -MAX_PEERS: 1 -EXPORT_AMBE: False -IP: bmproject.freedmr.uk -PORT: 54915 -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: 10 -SINGLE_MODE: True -VOICE_IDENT: False -TS1_STATIC: -TS2_STATIC: -DEFAULT_REFLECTOR: 0 -GENERATOR: 1 -ANNOUNCEMENT_LANGUAGE:es_ES -ALLOW_UNREG_ID: True -PROXY_CONTROL: False - - -[PLAY] -MODE: PEER -ENABLED: True -LOOSE: False -EXPORT_AMBE: False -IP: -PORT: 54002 -MASTER_IP: localhost -MASTER_PORT: 54001 -PASSPHRASE: homebrew -CALLSIGN: M0XFD -RADIO_ID: 2340210 -RX_FREQ: 449000000 -TX_FREQ: 444000000 -TX_POWER: 25 -COLORCODE: 1 -SLOTS: 1 -LATITUDE: 38.0000 -LONGITUDE: -095.0000 -HEIGHT: 75 -LOCATION: Anywhere, USA -DESCRIPTION: play_file.py -URL: www.w1abc.org -SOFTWARE_ID: 20170620 -PACKAGE_ID: MMDVM_FreeDMR -GROUP_HANGTIME: 5 -OPTIONS: -USE_ACL: True -SUB_ACL: DENY:1 -TGID_TS1_ACL: PERMIT:ALL -TGID_TS2_ACL: PERMIT:ALL -ANNOUNCEMENT_LANGUAGE: en_GB - diff --git a/playback_file.py b/playback_file.py index 2d13158..cf76cdf 100644 --- a/playback_file.py +++ b/playback_file.py @@ -209,7 +209,7 @@ if __name__ == '__main__': # Ensure we have a path for the config file, if one wasn't specified, then use the default (top of file) if not cli_args.CONFIG_FILE: - cli_args.CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/playback_file.cfg' + cli_args.CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/config/playback_file.cfg' # Call the external routine to build the configuration dictionary CONFIG = config.build_config(cli_args.CONFIG_FILE) diff --git a/proxy_db.py b/proxy_db.py new file mode 100644 index 0000000..6d062cd --- /dev/null +++ b/proxy_db.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# +############################################################################### +# Copyright (C) 2021-2022 Christian Quiroz, OA4DOA +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +############################################################################### +import sys + +from twisted.enterprise import adbapi +from twisted.internet.defer import inlineCallbacks + + +__author__ = 'Christian Quiroz, OA4DOA' +__version__ = '1.0.0' +__copyright__ = 'Copyright (c) 2021-2022 Christian Quiroz, OA4DOA' +__license__ = 'GNU GPLv3' +__maintainer__ = 'Christian Quiroz, OA4DOA' +__email__ = 'adm@dmr-peru.pe' + + +class ProxyDB: + def __init__(self, host, user, psswd, db_name, port): + self.db_name = db_name + self.dbpool = adbapi.ConnectionPool("MySQLdb", host, user, psswd, db_name, + port=port, charset="utf8mb4") + + @inlineCallbacks + def make_clients_tbl(self): + try: + yield self.dbpool.runOperation( + ''' CREATE TABLE IF NOT EXISTS Clients( + int_id INT UNIQUE PRIMARY KEY NOT NULL, + dmr_id TINYBLOB NOT NULL, + callsign VARCHAR(10) NOT NULL, + host VARCHAR(15), + options VARCHAR(100), + opt_rcvd TINYINT(1) DEFAULT False NOT NULL, + mode TINYINT(1) DEFAULT 4 NOT NULL, + logged_in TINYINT(1) DEFAULT False NOT NULL, + modified TINYINT(1) DEFAULT False NOT NULL, + psswd BLOB(256), + last_seen INT NOT NULL) CHARSET=utf8mb4''') + + except Exception as err: + print(f"make_clientss_tbl error: {err}") + + @inlineCallbacks + def test_db(self, _reactor): + try: + res = yield self.dbpool.runQuery("SELECT 1") + if res: + self.updt_tbl("start") + print("Database connection test: OK") + + except Exception as err: + if _reactor.running: + print(f"Database connection error: {err}, stopping the reactor.") + _reactor.stop() + else: + sys.exit(f"Database connection error: {err}, exiting.") + + @inlineCallbacks + def ins_conf(self, int_id, dmr_id, callsign, host, mode): + try: + yield self.dbpool.runOperation( + '''INSERT IGNORE INTO Clients ( + int_id, dmr_id, callsign, host, mode, logged_in, last_seen, psswd) + VALUES (%s, %s, %s, %s, %s, True, UNIX_TIMESTAMP(), NULL) ON DUPLICATE KEY UPDATE + callsign = %s, host = %s, mode = %s, logged_in = True, opt_rcvd = False, + last_seen = UNIX_TIMESTAMP(), psswd = NULL''', + (int_id, dmr_id, callsign, host, mode, callsign, host, mode)) + + except Exception as err: + print(f"ins_conf error: {err}") + + @inlineCallbacks + def clean_tbl(self): + try: + yield self.dbpool.runOperation( + "DELETE FROM Clients WHERE last_seen < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 7 DAY))") + + except Exception as err: + print(f"clean_tbl error: {err}") + + def slct_db(self): + return self.dbpool.runQuery( + "SELECT dmr_id, options FROM Clients WHERE modified = True and logged_in = True") + + def slct_opt(self, _peer_id): + return self.dbpool.runQuery("SELECT options FROM Clients WHERE dmr_id = %s", (_peer_id,)) + + @inlineCallbacks + def updt_tbl(self, actn, dmr_id=None, psswd=None): + try: + if actn == "start": + yield self.dbpool.runOperation("UPDATE Clients SET logged_in=False, opt_rcvd=False") + elif actn == "opt_rcvd": + yield self.dbpool.runOperation( + "UPDATE Clients SET opt_rcvd = True, options = NULL WHERE dmr_id = %s", + (dmr_id,)) + elif actn == "last_seen": + yield self.dbpool.runOperation( + "UPDATE Clients SET last_seen = UNIX_TIMESTAMP() WHERE dmr_id = %s and logged_in = True", + (dmr_id,)) + elif actn == "log_out": + yield self.dbpool.runOperation( + "UPDATE Clients SET logged_in = False, modified = False WHERE dmr_id = %s", + (dmr_id,)) + elif actn == "rst_mod": + yield self.dbpool.runOperation( + "UPDATE Clients SET modified = False WHERE dmr_id = %s", (dmr_id,)) + elif actn == "psswd": + yield self.dbpool.runOperation( + "UPDATE Clients SET psswd = %s WHERE dmr_id = %s", (psswd, dmr_id)) + + except Exception as err: + print(f"updt_tbl error: {err}") + + @inlineCallbacks + def updt_lstseen(self, dmrid_list): + try: + def db_actn(txn): + txn.executemany( + "UPDATE Clients SET last_seen = UNIX_TIMESTAMP() WHERE dmr_id = %s", dmrid_list) + yield self.dbpool.runInteraction(db_actn) + + except Exception as err: + print(f"updt_lstseen error: {err}") + + +if __name__ == "__main__": + db_test = ProxyDB('localhost', 'root', '', 'test', 3306) + print(db_test) + \ No newline at end of file