diff --git a/docker/routlin-dash/app/action_apply_banned_ips.py b/docker/routlin-dash/app/action_apply_banned_ips.py index d5ba9bf..51b5331 100644 --- a/docker/routlin-dash/app/action_apply_banned_ips.py +++ b/docker/routlin-dash/app/action_apply_banned_ips.py @@ -2,7 +2,7 @@ import copy from flask import Blueprint, request, redirect, flash from auth import require_level -from config_utils import load_core, save_core_with_snapshot, verify_core_hash +from config_utils import load_config, save_config_with_snapshot, verify_config_hash import sanitize import validation as validate @@ -19,7 +19,7 @@ def _row_index(): def _hash_ok(): - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return False return True @@ -47,17 +47,17 @@ def add_banned_ip(): if not _hash_ok(): return redirect(VIEW) - core = load_core() + cfg = load_config() entry = {'description': description, 'ip': ip, 'enabled': True} - core.setdefault('banned_ips', []).append(entry) - errors = validate.validate_config(core) + cfg.setdefault('banned_ips', []).append(entry) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='banned_ips', key=ip, operation='add', before=None, after=entry, description=f'Added banned IP: {ip}', @@ -75,23 +75,23 @@ def toggle_banned_ip(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('banned_ips', []) + cfg = load_config() + items = cfg.get('banned_ips', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) old_enabled = items[idx].get('enabled', True) items[idx]['enabled'] = not old_enabled - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) action = 'Enabled' if not old_enabled else 'Disabled' - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='banned_ips', key=items[idx]['ip'], operation='toggle', before={'enabled': old_enabled}, after={'enabled': not old_enabled}, description=f'{action} banned IP: {items[idx]["ip"]}', @@ -116,22 +116,22 @@ def edit_banned_ip(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('banned_ips', []) + cfg = load_config() + items = cfg.get('banned_ips', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) before = copy.deepcopy(items[idx]) items[idx].update({'description': description, 'ip': ip, 'enabled': enabled}) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='banned_ips', key=ip, operation='edit', before=before, after=copy.deepcopy(items[idx]), description=f'Edited banned IP: {ip}', @@ -149,21 +149,21 @@ def delete_banned_ip(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('banned_ips', []) + cfg = load_config() + items = cfg.get('banned_ips', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) removed = items.pop(idx) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='banned_ips', key=removed['ip'], operation='delete', before=removed, after=None, description=f'Deleted banned IP: {removed["ip"]}', diff --git a/docker/routlin-dash/app/action_apply_dhcp_reservations.py b/docker/routlin-dash/app/action_apply_dhcp_reservations.py index 8ddf60e..5f2dbac 100644 --- a/docker/routlin-dash/app/action_apply_dhcp_reservations.py +++ b/docker/routlin-dash/app/action_apply_dhcp_reservations.py @@ -3,7 +3,7 @@ import ipaddress from flask import Blueprint, request, redirect, flash from auth import require_level -from config_utils import load_core, save_core_with_snapshot, verify_core_hash +from config_utils import load_config, save_config_with_snapshot, verify_config_hash import sanitize import validation as validate @@ -20,7 +20,7 @@ def _row_index(): def _hash_ok(): - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return False return True @@ -86,8 +86,8 @@ def add_dhcp_reservation(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - vlans = core.get('vlans', []) + cfg = load_config() + vlans = cfg.get('vlans', []) vlan = next((v for v in vlans if v.get('name') == vlan_name), None) if vlan is None: flash(f'The configuration has not been saved because VLAN "{vlan_name}" was not found.', 'error') @@ -107,14 +107,14 @@ def add_dhcp_reservation(): 'enabled': True, } vlan.setdefault('reservations', []).append(entry) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path=f'vlans.{vlan_name}.reservations', key=mac, operation='add', before=None, after=entry, description=f'Added DHCP reservation: {hostname or mac} ({ip})', @@ -132,8 +132,8 @@ def toggle_dhcp_reservation(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - vlans = core.get('vlans', []) + cfg = load_config() + vlans = cfg.get('vlans', []) vi, ri = _flat_index_to_vlan_res(vlans, idx) if vi is None: flash('Entry not found.', 'error') @@ -142,7 +142,7 @@ def toggle_dhcp_reservation(): res = vlans[vi]['reservations'][ri] old_enabled = res.get('enabled', True) res['enabled'] = not old_enabled - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') @@ -150,8 +150,8 @@ def toggle_dhcp_reservation(): vlan_name = vlans[vi]['name'] action = 'Enabled' if not old_enabled else 'Disabled' - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path=f'vlans.{vlan_name}.reservations', key=res['mac'], operation='toggle', before={'enabled': old_enabled}, after={'enabled': not old_enabled}, description=f'{action} DHCP reservation: {res.get("hostname") or res["mac"]}', @@ -181,8 +181,8 @@ def edit_dhcp_reservation(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - vlans = core.get('vlans', []) + cfg = load_config() + vlans = cfg.get('vlans', []) vi, ri = _flat_index_to_vlan_res(vlans, idx) if vi is None: flash('Entry not found.', 'error') @@ -203,15 +203,15 @@ def edit_dhcp_reservation(): 'radius_client': radius_client, 'enabled': 'enabled' in request.form, }) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) vlan_name = vlans[vi]['name'] - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path=f'vlans.{vlan_name}.reservations', key=mac, operation='edit', before=before, after=copy.deepcopy(res), description=f'Edited DHCP reservation: {hostname or mac} ({ip})', @@ -229,8 +229,8 @@ def delete_dhcp_reservation(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - vlans = core.get('vlans', []) + cfg = load_config() + vlans = cfg.get('vlans', []) vi, ri = _flat_index_to_vlan_res(vlans, idx) if vi is None: flash('Entry not found.', 'error') @@ -238,14 +238,14 @@ def delete_dhcp_reservation(): vlan_name = vlans[vi]['name'] removed = vlans[vi]['reservations'].pop(ri) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path=f'vlans.{vlan_name}.reservations', key=removed['mac'], operation='delete', before=removed, after=None, description=f'Deleted DHCP reservation: {removed.get("hostname") or removed["mac"]}', diff --git a/docker/routlin-dash/app/action_apply_host_overrides.py b/docker/routlin-dash/app/action_apply_host_overrides.py index af7d748..bab498d 100644 --- a/docker/routlin-dash/app/action_apply_host_overrides.py +++ b/docker/routlin-dash/app/action_apply_host_overrides.py @@ -3,7 +3,7 @@ import ipaddress from flask import Blueprint, request, redirect, flash from auth import require_level -from config_utils import load_core, save_core_with_snapshot, verify_core_hash +from config_utils import load_config, save_config_with_snapshot, verify_config_hash import sanitize import validation as validate @@ -12,9 +12,9 @@ bp = Blueprint('action_apply_host_overrides', __name__) VIEW = '/view/view_host_overrides' -def _vlan_networks(core): +def _vlan_networks(cfg): nets = [] - for v in core.get('vlans', []): + for v in cfg.get('vlans', []): subnet = v.get('subnet', '') mask = v.get('subnet_mask', '') if subnet and mask: @@ -25,12 +25,12 @@ def _vlan_networks(core): return nets -def _ip_in_vlan(ip_str, core): +def _ip_in_vlan(ip_str, cfg): try: addr = ipaddress.IPv4Address(ip_str) except ValueError: return False - nets = _vlan_networks(core) + nets = _vlan_networks(cfg) return not nets or any(addr in net for net in nets) @@ -42,7 +42,7 @@ def _row_index(): def _hash_ok(): - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return False return True @@ -61,21 +61,21 @@ def add_host_override(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - if not _ip_in_vlan(ip, core): + cfg = load_config() + if not _ip_in_vlan(ip, cfg): flash('IP address does not fall within any configured VLAN subnet.', 'error') return redirect(VIEW) entry = {'description': description, 'host': host, 'ip': ip, 'enabled': True} - core.setdefault('host_overrides', []).append(entry) - errors = validate.validate_config(core) + cfg.setdefault('host_overrides', []).append(entry) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='host_overrides', key=host, operation='add', before=None, after=entry, description=f'Added host override: {host} → {ip}', @@ -93,23 +93,23 @@ def toggle_host_override(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('host_overrides', []) + cfg = load_config() + items = cfg.get('host_overrides', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) old_enabled = items[idx].get('enabled', True) items[idx]['enabled'] = not old_enabled - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) action = 'Enabled' if not old_enabled else 'Disabled' - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='host_overrides', key=items[idx]['host'], operation='toggle', before={'enabled': old_enabled}, after={'enabled': not old_enabled}, description=f'{action} host override: {items[idx]["host"]}', @@ -136,26 +136,26 @@ def edit_host_override(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - if not _ip_in_vlan(ip, core): + cfg = load_config() + if not _ip_in_vlan(ip, cfg): flash('IP address does not fall within any configured VLAN subnet.', 'error') return redirect(VIEW) - items = core.get('host_overrides', []) + items = cfg.get('host_overrides', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) before = copy.deepcopy(items[idx]) items[idx].update({'description': description, 'host': host, 'ip': ip, 'enabled': enabled}) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='host_overrides', key=host, operation='edit', before=before, after=copy.deepcopy(items[idx]), description=f'Edited host override: {host} → {ip}', @@ -173,21 +173,21 @@ def delete_host_override(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('host_overrides', []) + cfg = load_config() + items = cfg.get('host_overrides', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) removed = items.pop(idx) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='host_overrides', key=removed['host'], operation='delete', before=removed, after=None, description=f'Deleted host override: {removed["host"]}', diff --git a/docker/routlin-dash/app/action_apply_inter_vlan.py b/docker/routlin-dash/app/action_apply_inter_vlan.py index ed5fb12..92ea8a8 100644 --- a/docker/routlin-dash/app/action_apply_inter_vlan.py +++ b/docker/routlin-dash/app/action_apply_inter_vlan.py @@ -2,7 +2,7 @@ import copy from flask import Blueprint, request, redirect, flash from auth import require_level -from config_utils import load_core, save_core_with_snapshot, verify_core_hash +from config_utils import load_config, save_config_with_snapshot, verify_config_hash import sanitize import validation as validate @@ -21,7 +21,7 @@ def _row_index(): def _hash_ok(): - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return False return True @@ -86,17 +86,17 @@ def add_inter_vlan(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - core.setdefault('inter_vlan_exceptions', []).append(entry) - errors = validate.validate_config(core) + cfg = load_config() + cfg.setdefault('inter_vlan_exceptions', []).append(entry) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) key = _entry_key(entry) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='inter_vlan_exceptions', key=key, operation='add', before=None, after=entry, description=f'Added inter-VLAN rule: {key}', @@ -114,15 +114,15 @@ def toggle_inter_vlan(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('inter_vlan_exceptions', []) + cfg = load_config() + items = cfg.get('inter_vlan_exceptions', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) old_enabled = items[idx].get('enabled', True) items[idx]['enabled'] = not old_enabled - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') @@ -130,8 +130,8 @@ def toggle_inter_vlan(): key = _entry_key(items[idx]) action = 'Enabled' if not old_enabled else 'Disabled' - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='inter_vlan_exceptions', key=key, operation='toggle', before={'enabled': old_enabled}, after={'enabled': not old_enabled}, description=f'{action} inter-VLAN rule: {key}', @@ -153,8 +153,8 @@ def edit_inter_vlan(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('inter_vlan_exceptions', []) + cfg = load_config() + items = cfg.get('inter_vlan_exceptions', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) @@ -162,15 +162,15 @@ def edit_inter_vlan(): before = copy.deepcopy(items[idx]) items[idx] = entry items[idx]['enabled'] = request.form.get('enabled') == 'on' - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) key = _entry_key(entry) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='inter_vlan_exceptions', key=key, operation='edit', before=before, after=copy.deepcopy(items[idx]), description=f'Edited inter-VLAN rule: {key}', @@ -188,22 +188,22 @@ def delete_inter_vlan(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('inter_vlan_exceptions', []) + cfg = load_config() + items = cfg.get('inter_vlan_exceptions', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) removed = items.pop(idx) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) key = _entry_key(removed) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='inter_vlan_exceptions', key=key, operation='delete', before=removed, after=None, description=f'Deleted inter-VLAN rule: {key}', diff --git a/docker/routlin-dash/app/action_apply_mdns.py b/docker/routlin-dash/app/action_apply_mdns.py index 9b111b0..6b8daa6 100644 --- a/docker/routlin-dash/app/action_apply_mdns.py +++ b/docker/routlin-dash/app/action_apply_mdns.py @@ -2,7 +2,7 @@ import copy from flask import Blueprint, request, redirect, flash from auth import require_level -from config_utils import load_core, save_core_with_snapshot, verify_core_hash +from config_utils import load_config, save_config_with_snapshot, verify_config_hash import sanitize import validation as validate @@ -14,31 +14,31 @@ bp = Blueprint('action_apply_mdns', __name__) def apply_mdns(): mdns_enabled = 'mdns_enabled' in request.form - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return redirect('/view/view_mdns') - core = load_core() + cfg = load_config() mdns_reflect_vlans = sanitize.filterlist( request.form.getlist('mdns_reflect_vlans'), - {v.get('name') for v in core.get('vlans', [])}, + {v.get('name') for v in cfg.get('vlans', [])}, ) - before = copy.deepcopy(core.get('mdns_reflection', {})) - core.setdefault('mdns_reflection', {}).update({ + before = copy.deepcopy(cfg.get('mdns_reflection', {})) + cfg.setdefault('mdns_reflection', {}).update({ 'enabled': mdns_enabled, 'reflect_vlans': mdns_reflect_vlans, }) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect('/view/view_mdns') - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='mdns_reflection', key='global', operation='edit', - before=before or None, after=copy.deepcopy(core['mdns_reflection']), + before=before or None, after=copy.deepcopy(cfg['mdns_reflection']), description='Updated mDNS reflection settings', ), 'success') return redirect('/view/view_mdns') diff --git a/docker/routlin-dash/app/action_apply_port_forwarding.py b/docker/routlin-dash/app/action_apply_port_forwarding.py index 37bf3d7..ea13d3f 100644 --- a/docker/routlin-dash/app/action_apply_port_forwarding.py +++ b/docker/routlin-dash/app/action_apply_port_forwarding.py @@ -2,7 +2,7 @@ import copy from flask import Blueprint, request, redirect, flash from auth import require_level -from config_utils import load_core, save_core_with_snapshot, verify_core_hash +from config_utils import load_config, save_config_with_snapshot, verify_config_hash import sanitize import validation as validate @@ -21,7 +21,7 @@ def _row_index(): def _hash_ok(): - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return False return True @@ -82,17 +82,17 @@ def add_port_forward(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - core.setdefault('port_forwarding', []).append(entry) - errors = validate.validate_config(core) + cfg = load_config() + cfg.setdefault('port_forwarding', []).append(entry) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) key = f'{entry["protocol"]}:{entry["dest_port"]}' - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='port_forwarding', key=key, operation='add', before=None, after=entry, description=f'Added port forward: {key} → {entry["nat_ip"]}:{entry["nat_port"]}', @@ -110,15 +110,15 @@ def toggle_port_forward(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('port_forwarding', []) + cfg = load_config() + items = cfg.get('port_forwarding', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) old_enabled = items[idx].get('enabled', True) items[idx]['enabled'] = not old_enabled - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') @@ -126,8 +126,8 @@ def toggle_port_forward(): key = f'{items[idx]["protocol"]}:{items[idx]["dest_port"]}' action = 'Enabled' if not old_enabled else 'Disabled' - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='port_forwarding', key=key, operation='toggle', before={'enabled': old_enabled}, after={'enabled': not old_enabled}, description=f'{action} port forward: {key}', @@ -149,8 +149,8 @@ def edit_port_forward(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('port_forwarding', []) + cfg = load_config() + items = cfg.get('port_forwarding', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) @@ -158,15 +158,15 @@ def edit_port_forward(): before = copy.deepcopy(items[idx]) items[idx] = entry items[idx]['enabled'] = request.form.get('enabled') == 'on' - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) key = f'{entry["protocol"]}:{entry["dest_port"]}' - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='port_forwarding', key=key, operation='edit', before=before, after=copy.deepcopy(items[idx]), description=f'Edited port forward: {key} → {entry["nat_ip"]}:{entry["nat_port"]}', @@ -184,22 +184,22 @@ def delete_port_forward(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('port_forwarding', []) + cfg = load_config() + items = cfg.get('port_forwarding', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) removed = items.pop(idx) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) key = f'{removed["protocol"]}:{removed["dest_port"]}' - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='port_forwarding', key=key, operation='delete', before=removed, after=None, description=f'Deleted port forward: {key}', diff --git a/docker/routlin-dash/app/action_apply_vlans.py b/docker/routlin-dash/app/action_apply_vlans.py index 53f2e02..46c5674 100644 --- a/docker/routlin-dash/app/action_apply_vlans.py +++ b/docker/routlin-dash/app/action_apply_vlans.py @@ -2,7 +2,7 @@ import copy from flask import Blueprint, request, redirect, flash from auth import require_level -from config_utils import load_core, save_core_with_snapshot, verify_core_hash +from config_utils import load_config, save_config_with_snapshot, verify_config_hash import sanitize import validation as validate @@ -22,7 +22,7 @@ def _row_index(): def _hash_ok(): - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return False return True @@ -40,7 +40,7 @@ def add_vlan(): dnsmasq_log_queries = 'dnsmasq_log_queries' in request.form use_blocklists = sanitize.filterlist( request.form.getlist('use_blocklists'), - {b.get('name') for b in load_core().get('dns_blocking', {}).get('blocklists', [])}, + {b.get('name') for b in load_config().get('dns_blocking', {}).get('blocklists', [])}, ) if not name: @@ -61,8 +61,8 @@ def add_vlan(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - vlans = core.setdefault('vlans', []) + cfg = load_config() + vlans = cfg.setdefault('vlans', []) if any(validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) == vlan_id for v in vlans): flash(f'VLAN {vlan_id} (derived from subnet) already exists.', 'error') @@ -86,14 +86,14 @@ def add_vlan(): else: entry['reservations'] = [] vlans.append(entry) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='vlans', key=name, operation='add', before=None, after={k: entry[k] for k in _VLAN_FIELDS if k in entry}, description=f'Added VLAN: {name} ({subnet}/{subnet_mask})', @@ -116,7 +116,7 @@ def edit_vlan(): dnsmasq_log_queries = 'dnsmasq_log_queries' in request.form use_blocklists = sanitize.filterlist( request.form.getlist('use_blocklists'), - {b.get('name') for b in load_core().get('dns_blocking', {}).get('blocklists', [])}, + {b.get('name') for b in load_config().get('dns_blocking', {}).get('blocklists', [])}, ) subnet_mask_raw = request.form.get('subnet_mask') @@ -137,8 +137,8 @@ def edit_vlan(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - vlans = core.get('vlans', []) + cfg = load_config() + vlans = cfg.get('vlans', []) if idx < 0 or idx >= len(vlans): flash('VLAN not found.', 'error') return redirect(VIEW) @@ -179,14 +179,14 @@ def edit_vlan(): 'mdns_reflection': mdns_reflection, 'use_blocklists': use_blocklists, }) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='vlans', key=name, operation='edit', before=before, after={k: existing.get(k) for k in _VLAN_FIELDS}, description=f'Edited VLAN: {name}', @@ -204,21 +204,21 @@ def delete_vlan(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - vlans = core.get('vlans', []) + cfg = load_config() + vlans = cfg.get('vlans', []) if idx < 0 or idx >= len(vlans): flash('VLAN not found.', 'error') return redirect(VIEW) removed = vlans.pop(idx) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path='vlans', key=removed['name'], operation='delete', before={k: removed.get(k) for k in _VLAN_FIELDS}, after=None, diff --git a/docker/routlin-dash/app/action_apply_vpn.py b/docker/routlin-dash/app/action_apply_vpn.py index ff4783e..83c8075 100644 --- a/docker/routlin-dash/app/action_apply_vpn.py +++ b/docker/routlin-dash/app/action_apply_vpn.py @@ -5,7 +5,7 @@ import re from flask import Blueprint, make_response, redirect, flash, request from auth import require_level -from config_utils import load_core, save_core_with_snapshot, verify_core_hash, CONFIGS_DIR, WEB_APP_DISPLAY_NAME +from config_utils import load_config, save_config_with_snapshot, verify_config_hash, CONFIGS_DIR, WEB_APP_DISPLAY_NAME import sanitize import validation as validate @@ -16,17 +16,17 @@ _MTU_MIN = 576 _MTU_MAX = 9000 -def _wg_vlan(core): - return next((v for v in core.get('vlans', []) if v.get('is_vpn')), None) +def _wg_vlan(cfg): + return next((v for v in cfg.get('vlans', []) if v.get('is_vpn')), None) -def _wg_vlan_by_name(core, name): - return next((v for v in core.get('vlans', []) if v.get('is_vpn') and v.get('name') == name), None) +def _wg_vlan_by_name(cfg, name): + return next((v for v in cfg.get('vlans', []) if v.get('is_vpn') and v.get('name') == name), None) -def _find_peer_by_flat_idx(core, flat_idx): +def _find_peer_by_flat_idx(cfg, flat_idx): i = 0 - for vlan in core.get('vlans', []): + for vlan in cfg.get('vlans', []): if not vlan.get('is_vpn'): continue peers = vlan.get('peers', []) @@ -37,8 +37,8 @@ def _find_peer_by_flat_idx(core, flat_idx): return None, None -def _wg_iface(vlan, core): - wg_vlans = [v for v in core.get('vlans', []) if v.get('is_vpn')] +def _wg_iface(vlan, cfg): + wg_vlans = [v for v in cfg.get('vlans', []) if v.get('is_vpn')] idx = next((i for i, v in enumerate(wg_vlans) if v is vlan), 0) return f'wg{idx}' @@ -51,7 +51,7 @@ def _row_index(): def _hash_ok(): - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return False return True @@ -115,8 +115,8 @@ def _build_client_conf(vlan, peer_name, peer_ip, private_key, server_pubkey): def _conf_response(vlan, peer_name, peer_ip, private_key): - core = load_core() - iface = _wg_iface(vlan, core) + cfg = load_config() + iface = _wg_iface(vlan, cfg) server_pub = _server_pubkey(iface) if not server_pub: flash('Peer saved. Run sudo python3 ~/routlin/core.py --apply to generate the server ' @@ -164,13 +164,13 @@ def apply_vpn(): if not _hash_ok(): return redirect(_VIEW) - core = load_core() - vpn_vlan = _wg_vlan(core) + cfg = load_config() + vpn_vlan = _wg_vlan(cfg) if vpn_vlan is None: flash('No WireGuard VLAN found in configuration.', 'error') return redirect(_VIEW) - for v in core.get('vlans', []): + for v in cfg.get('vlans', []): if v.get('is_vpn') and v is not vpn_vlan and v.get('vpn_information', {}).get('listen_port') == listen_port: flash(f'Listen port {listen_port} is already used by another VPN VLAN.', 'error') return redirect(_VIEW) @@ -191,15 +191,15 @@ def apply_vpn(): else: overrides.pop('mtu', None) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(_VIEW) vlan_name = vpn_vlan['name'] - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path=f'vlans.{vlan_name}.vpn_information', key=vlan_name, operation='edit', before=before_info or None, after=copy.deepcopy(info), description=f'Updated VPN configuration for {vlan_name}', @@ -229,8 +229,8 @@ def add_vpn_peer(): if not _hash_ok(): return redirect(_VIEW) - core = load_core() - vpn_vlan = _wg_vlan_by_name(core, peer_vlan_nm) + cfg = load_config() + vpn_vlan = _wg_vlan_by_name(cfg, peer_vlan_nm) if vpn_vlan is None: flash(f'VPN VLAN "{peer_vlan_nm}" not found.', 'error') return redirect(_VIEW) @@ -247,7 +247,7 @@ def add_vpn_peer(): if any(p.get('name') == peer_name for p in peers): flash(f'A peer named "{peer_name}" already exists.', 'error') return redirect(_VIEW) - for v in core.get('vlans', []): + for v in cfg.get('vlans', []): if not v.get('is_vpn'): continue if any(p.get('ip') == peer_ip for p in v.get('peers', [])): @@ -263,14 +263,14 @@ def add_vpn_peer(): 'enabled': enabled, } peers.append(entry) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(_VIEW) - save_core_with_snapshot( - core, + save_config_with_snapshot( + cfg, path=f'vlans.{peer_vlan_nm}.peers', key=peer_name, operation='add', before=None, after={k: v for k, v in entry.items() if k != 'public_key'}, description=f'Added VPN peer: {peer_name} ({peer_ip})', @@ -297,8 +297,8 @@ def edit_vpn_peer(): if not _hash_ok(): return redirect(_VIEW) - core = load_core() - vlan, peer_idx = _find_peer_by_flat_idx(core, flat_idx) + cfg = load_config() + vlan, peer_idx = _find_peer_by_flat_idx(cfg, flat_idx) if vlan is None: flash('Peer not found.', 'error') return redirect(_VIEW) @@ -310,15 +310,15 @@ def edit_vpn_peer(): before = copy.deepcopy({k: peers[peer_idx].get(k) for k in ('name', 'split_tunnel', 'enabled')}) peers[peer_idx].update({'name': peer_name, 'split_tunnel': split_tunnel, 'enabled': enabled}) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(_VIEW) vlan_name = vlan['name'] - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path=f'vlans.{vlan_name}.peers', key=peer_name, operation='edit', before=before, after={'name': peer_name, 'split_tunnel': split_tunnel, 'enabled': enabled}, description=f'Edited VPN peer: {peer_name}', @@ -336,8 +336,8 @@ def toggle_vpn_peer(): if not _hash_ok(): return redirect(_VIEW) - core = load_core() - vlan, peer_idx = _find_peer_by_flat_idx(core, flat_idx) + cfg = load_config() + vlan, peer_idx = _find_peer_by_flat_idx(cfg, flat_idx) if vlan is None: flash('Peer not found.', 'error') return redirect(_VIEW) @@ -345,7 +345,7 @@ def toggle_vpn_peer(): peers = vlan.get('peers', []) old_enabled = peers[peer_idx].get('enabled', True) peers[peer_idx]['enabled'] = not old_enabled - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') @@ -354,8 +354,8 @@ def toggle_vpn_peer(): peer_name = peers[peer_idx]['name'] vlan_name = vlan['name'] action = 'Enabled' if not old_enabled else 'Disabled' - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path=f'vlans.{vlan_name}.peers', key=peer_name, operation='toggle', before={'enabled': old_enabled}, after={'enabled': not old_enabled}, description=f'{action} VPN peer: {peer_name}', @@ -373,23 +373,23 @@ def delete_vpn_peer(): if not _hash_ok(): return redirect(_VIEW) - core = load_core() - vlan, peer_idx = _find_peer_by_flat_idx(core, flat_idx) + cfg = load_config() + vlan, peer_idx = _find_peer_by_flat_idx(cfg, flat_idx) if vlan is None: flash('Peer not found.', 'error') return redirect(_VIEW) peers = vlan.get('peers', []) removed = peers.pop(peer_idx) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(_VIEW) vlan_name = vlan['name'] - flash(save_core_with_snapshot( - core, + flash(save_config_with_snapshot( + cfg, path=f'vlans.{vlan_name}.peers', key=removed['name'], operation='delete', before={k: removed.get(k) for k in ('name', 'ip', 'split_tunnel', 'enabled')}, after=None, @@ -408,8 +408,8 @@ def regenerate_vpn_peer(): if not _hash_ok(): return redirect(_VIEW) - core = load_core() - vlan, peer_idx = _find_peer_by_flat_idx(core, flat_idx) + cfg = load_config() + vlan, peer_idx = _find_peer_by_flat_idx(cfg, flat_idx) if vlan is None: flash('Peer not found.', 'error') return redirect(_VIEW) @@ -418,15 +418,15 @@ def regenerate_vpn_peer(): peer = vlan['peers'][peer_idx] old_pub_key = peer.get('public_key', '') peer['public_key'] = public_key - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(_VIEW) vlan_name = vlan['name'] - save_core_with_snapshot( - core, + save_config_with_snapshot( + cfg, path=f'vlans.{vlan_name}.peers', key=peer['name'], operation='regenerate', before={'public_key': old_pub_key}, after={'public_key': public_key}, description=f'Regenerated keypair for VPN peer: {peer["name"]}', diff --git a/docker/routlin-dash/app/action_ddns.py b/docker/routlin-dash/app/action_ddns.py index ec0a21f..8670f91 100644 --- a/docker/routlin-dash/app/action_ddns.py +++ b/docker/routlin-dash/app/action_ddns.py @@ -1,8 +1,8 @@ +import copy import os -import re from flask import Blueprint, request, redirect, flash, send_file, abort from auth import require_level -from config_utils import load_core, save_core, verify_core_hash, queued_msg, CONFIGS_DIR +from config_utils import load_config, verify_config_hash, save_config_with_snapshot, CONFIGS_DIR import sanitize import validation as validate @@ -29,6 +29,10 @@ def ddns_cardaddaccount_add(): flash('Unknown provider type.', 'error') return redirect(VIEW) + if not verify_config_hash(request.form.get('config_hash', '')): + flash('Configuration was modified by another session. Please refresh and try again.', 'error') + return redirect(VIEW) + entry = { 'description': description, 'provider': provider_type, @@ -41,10 +45,14 @@ def ddns_cardaddaccount_add(): else: entry['api_token'] = request.form.get('api_token', '').strip() - core = load_core() - core.setdefault('ddns', {}).setdefault('providers', []).append(entry) - save_core(core) - flash(f'DDNS provider "{description}" added.', 'success') + cfg = load_config() + cfg.setdefault('ddns', {}).setdefault('providers', []).append(entry) + flash(save_config_with_snapshot( + cfg, path='ddns', key=description, operation='add', + before=None, after=copy.deepcopy(entry), + description=f'Added DDNS provider: {description}', + cmd='ddns update', + ), 'success') return redirect(VIEW) @@ -66,12 +74,17 @@ def ddns_tableaccounts_rowedit(): flash('Unknown provider type.', 'error') return redirect(VIEW) - core = load_core() - providers = core.setdefault('ddns', {}).setdefault('providers', []) + if not verify_config_hash(request.form.get('config_hash', '')): + flash('Configuration was modified by another session. Please refresh and try again.', 'error') + return redirect(VIEW) + + cfg = load_config() + providers = cfg.setdefault('ddns', {}).setdefault('providers', []) if row_index < 0 or row_index >= len(providers): flash('Invalid provider index.', 'error') return redirect(VIEW) + before = copy.deepcopy(providers[row_index]) entry = { 'description': description, 'provider': provider_type, @@ -85,8 +98,12 @@ def ddns_tableaccounts_rowedit(): entry['api_token'] = request.form.get('api_token', '').strip() providers[row_index] = entry - save_core(core) - flash('DDNS provider updated.', 'success') + flash(save_config_with_snapshot( + cfg, path='ddns', key=description, operation='edit', + before=before, after=copy.deepcopy(entry), + description=f'Edited DDNS provider: {description}', + cmd='ddns update', + ), 'success') return redirect(VIEW) @@ -99,15 +116,25 @@ def ddns_tableaccounts_rowdelete(): flash('Invalid row index.', 'error') return redirect(VIEW) - core = load_core() - providers = core.setdefault('ddns', {}).setdefault('providers', []) + if not verify_config_hash(request.form.get('config_hash', '')): + flash('Configuration was modified by another session. Please refresh and try again.', 'error') + return redirect(VIEW) + + cfg = load_config() + providers = cfg.setdefault('ddns', {}).setdefault('providers', []) if row_index < 0 or row_index >= len(providers): flash('Invalid provider index.', 'error') return redirect(VIEW) + before = copy.deepcopy(providers[row_index]) + description = before.get('description', str(row_index)) del providers[row_index] - save_core(core) - flash('DDNS provider deleted.', 'success') + flash(save_config_with_snapshot( + cfg, path='ddns', key=description, operation='delete', + before=before, after=None, + description=f'Deleted DDNS provider: {description}', + cmd='ddns update', + ), 'success') return redirect(VIEW) @@ -123,20 +150,27 @@ def ddns_cardipcheckinterval_save(): flash('Interval must be a whole number of minutes >= 1.', 'error') return redirect(VIEW) timer_interval = f'{mins}m' - if not verify_core_hash(request.form.get('config_hash', '')): + + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return redirect(VIEW) - core = load_core() - core.setdefault('ddns', {}).setdefault('general', {})['timer_interval'] = timer_interval - save_core(core) - flash(queued_msg('core apply'), 'success') + + cfg = load_config() + before = copy.deepcopy(cfg.get('ddns', {}).get('general', {})) + cfg.setdefault('ddns', {}).setdefault('general', {})['timer_interval'] = timer_interval + flash(save_config_with_snapshot( + cfg, path='ddns', key='general', operation='edit', + before=before, after=copy.deepcopy(cfg['ddns']['general']), + description='Updated DDNS check interval', + cmd='core apply', + ), 'success') return redirect(VIEW) @bp.route('/action/ddns_cardipcheckservices_save', methods=['POST']) @require_level('administrator') def ddns_cardipcheckservices_save(): - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return redirect(VIEW) @@ -147,13 +181,17 @@ def ddns_cardipcheckservices_save(): flash('At least one IP check service is required.', 'error') return redirect(VIEW) - services = [{'type': 'http', 'url': u} for u in http_services] + cfg = load_config() + before = copy.deepcopy(cfg.get('ddns', {}).get('ip_check_services', [])) + services = [{'type': 'http', 'url': u} for u in http_services] services += [{'type': 'dig', 'url': u} for u in dig_services] - - core = load_core() - core.setdefault('ddns', {})['ip_check_services'] = services - save_core(core) - flash('IP check services saved.', 'success') + cfg.setdefault('ddns', {})['ip_check_services'] = services + flash(save_config_with_snapshot( + cfg, path='ddns', key='ip_check_services', operation='edit', + before=before, after=copy.deepcopy(services), + description='Updated DDNS IP check services', + cmd='ddns update', + ), 'success') return redirect(VIEW) @@ -165,16 +203,23 @@ def ddns_cardlogging_save(): flash('Max Log Size must be a number >= 64.', 'error') return redirect(VIEW) log_errors_only = 'log_errors_only' in request.form - if not verify_core_hash(request.form.get('config_hash', '')): + + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return redirect(VIEW) - core = load_core() - core.setdefault('ddns', {}).setdefault('general', {}).update({ + + cfg = load_config() + before = copy.deepcopy(cfg.get('ddns', {}).get('general', {})) + cfg.setdefault('ddns', {}).setdefault('general', {}).update({ 'log_max_kb': log_max_kb, 'log_errors_only': log_errors_only, }) - save_core(core) - flash('DDNS log settings saved.', 'success') + flash(save_config_with_snapshot( + cfg, path='ddns', key='general', operation='edit', + before=before, after=copy.deepcopy(cfg['ddns']['general']), + description='Updated DDNS logging settings', + cmd='ddns update', + ), 'success') return redirect(VIEW) diff --git a/docker/routlin-dash/app/action_dnsblocking.py b/docker/routlin-dash/app/action_dnsblocking.py index f5266d8..4184604 100644 --- a/docker/routlin-dash/app/action_dnsblocking.py +++ b/docker/routlin-dash/app/action_dnsblocking.py @@ -1,7 +1,7 @@ import re from flask import Blueprint, request, redirect, flash from auth import require_level -from config_utils import load_core, save_core, verify_core_hash, queued_msg +from config_utils import load_config, save_config, verify_config_hash, queued_msg import sanitize import validation as validate @@ -20,7 +20,7 @@ def _row_index(): def _hash_ok(): - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return False return True @@ -62,19 +62,19 @@ def dnsblocking_tableblocklists_rowdelete(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('dns_blocking', {}).get('blocklists', []) + cfg = load_config() + items = cfg.get('dns_blocking', {}).get('blocklists', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) items.pop(idx) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - save_core(core) + save_config(cfg) flash(queued_msg('core apply'), 'success') return redirect(VIEW) @@ -95,8 +95,8 @@ def dnsblocking_tableblocklists_rowedit(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('dns_blocking', {}).get('blocklists', []) + cfg = load_config() + items = cfg.get('dns_blocking', {}).get('blocklists', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) @@ -107,12 +107,12 @@ def dnsblocking_tableblocklists_rowedit(): 'format': fields['format'], 'url': fields['url'], }) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - save_core(core) + save_config(cfg) flash(queued_msg('core apply'), 'success') return redirect(VIEW) @@ -128,8 +128,8 @@ def dnsblocking_cardaddblocklist_add(): if not _hash_ok(): return redirect(VIEW) - core = load_core() - blocklists = core.setdefault('dns_blocking', {}).setdefault('blocklists', []) + cfg = load_config() + blocklists = cfg.setdefault('dns_blocking', {}).setdefault('blocklists', []) if any(b.get('name', '').lower() == fields['name'].lower() for b in blocklists): flash('The configuration has not been saved because a blocklist with that name already exists.', 'error') @@ -142,12 +142,12 @@ def dnsblocking_cardaddblocklist_add(): 'url': fields['url'], 'save_as': _save_as_from_name(fields['name']), }) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - save_core(core) + save_config(cfg) flash(queued_msg('core apply'), 'success') return redirect(VIEW) @@ -162,13 +162,13 @@ def dnsblocking_cardblocklistrefresh_save(): flash('Daily Refresh Time must be a valid 24-hour time (e.g. 02:30).', 'error') return redirect(VIEW) - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return redirect(VIEW) - core = load_core() - core.setdefault('dns_blocking', {}).setdefault('general', {})['daily_execute_time_24hr_local'] = daily_execute_time - save_core(core) + cfg = load_config() + cfg.setdefault('dns_blocking', {}).setdefault('general', {})['daily_execute_time_24hr_local'] = daily_execute_time + save_config(cfg) flash(queued_msg('core apply'), 'success') return redirect(VIEW) @@ -192,21 +192,21 @@ def dnsblocking_cardlogging_save(): flash('Max Log Size must be a number >= 64.', 'error') return redirect(VIEW) - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return redirect(VIEW) - core = load_core() - core.setdefault('dns_blocking', {}).setdefault('general', {}).update({ + cfg = load_config() + cfg.setdefault('dns_blocking', {}).setdefault('general', {}).update({ 'log_max_kb': log_max_kb, 'log_errors_only': log_errors_only, }) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) - save_core(core) + save_config(cfg) flash(queued_msg('core apply'), 'success') return redirect(VIEW) diff --git a/docker/routlin-dash/app/action_networkinterfaces.py b/docker/routlin-dash/app/action_networkinterfaces.py index 1ec6410..0327cb9 100644 --- a/docker/routlin-dash/app/action_networkinterfaces.py +++ b/docker/routlin-dash/app/action_networkinterfaces.py @@ -2,7 +2,7 @@ import os from flask import Blueprint, request, redirect, flash from auth import require_level -from config_utils import load_core, save_core, verify_core_hash, queued_msg, queue_command +from config_utils import load_config, save_config, verify_config_hash, queued_msg, queue_command import sanitize import validation as validate @@ -44,7 +44,7 @@ def networkinterfaces_cardnetworkinterface_save(): flash('WAN and LAN interfaces must be different.', 'error') return redirect(_VIEW) - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return redirect(_VIEW) @@ -54,16 +54,16 @@ def networkinterfaces_cardnetworkinterface_save(): flash(f"Interface '{iface}' does not exist on this system.", 'error') return redirect(_VIEW) - core = load_core() - gen = core.setdefault('network_interfaces', {}) + cfg = load_config() + gen = cfg.setdefault('network_interfaces', {}) gen['wan_interface'] = wan gen['lan_interface'] = lan - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(_VIEW) - save_core(core) + save_config(cfg) flash(queued_msg('core apply'), 'success') return redirect(_VIEW) @@ -72,7 +72,7 @@ def networkinterfaces_cardnetworkinterface_save(): @bp.route('/action/networkinterfaces_cardinterfaceconfiguration_apply', methods=['POST']) @require_level('administrator') def networkinterfaces_cardinterfaceconfiguration_apply(): - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return redirect(_VIEW) diff --git a/docker/routlin-dash/app/action_upstreamdns.py b/docker/routlin-dash/app/action_upstreamdns.py index c5ca1e3..f1db200 100644 --- a/docker/routlin-dash/app/action_upstreamdns.py +++ b/docker/routlin-dash/app/action_upstreamdns.py @@ -1,6 +1,6 @@ from flask import Blueprint, request, redirect, flash from auth import require_level -from config_utils import load_core, save_core, verify_core_hash, queued_msg +from config_utils import load_config, save_config, verify_config_hash, queued_msg import sanitize import validation as validate @@ -28,27 +28,27 @@ def upstreamdns_cardupstreamdns_save(): return redirect(_VIEW) upstream_servers.append(clean) - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return redirect(_VIEW) - core = load_core() - current = core.get('upstream_dns', {}) + cfg = load_config() + current = cfg.get('upstream_dns', {}) if (strict_order == bool(current.get('strict_order', False)) and upstream_servers == current.get('upstream_servers', [])): flash('No changes detected.', 'info') return redirect(_VIEW) - core.setdefault('upstream_dns', {}).update({ + cfg.setdefault('upstream_dns', {}).update({ 'strict_order': strict_order, 'upstream_servers': upstream_servers, }) - errors = validate.validate_config(core) + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(_VIEW) - save_core(core) + save_config(cfg) flash(queued_msg('core apply'), 'success') return redirect(_VIEW) @@ -61,22 +61,22 @@ def upstreamdns_cardforwardingdnsservice_save(): flash('Cache Size must be a non-negative integer.', 'error') return redirect(_VIEW) - if not verify_core_hash(request.form.get('config_hash', '')): + if not verify_config_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return redirect(_VIEW) - core = load_core() - current = core.get('upstream_dns', {}) + cfg = load_config() + current = cfg.get('upstream_dns', {}) if cache_size == int(current.get('cache_size', 0)): flash('No changes detected.', 'info') return redirect(_VIEW) - core.setdefault('upstream_dns', {})['cache_size'] = cache_size - errors = validate.validate_config(core) + cfg.setdefault('upstream_dns', {})['cache_size'] = cache_size + errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(_VIEW) - save_core(core) + save_config(cfg) flash(queued_msg('core apply'), 'success') return redirect(_VIEW) diff --git a/docker/routlin-dash/app/config_utils.py b/docker/routlin-dash/app/config_utils.py index 06ec600..2ae1651 100644 --- a/docker/routlin-dash/app/config_utils.py +++ b/docker/routlin-dash/app/config_utils.py @@ -5,7 +5,7 @@ from flask import session CONFIGS_DIR = '/routlin_location' DATA_DIR = '/data' ACCOUNTS_FILE = f'{DATA_DIR}/authorized_accounts.json' -CORE_FILE = f'{CONFIGS_DIR}/core.json' +CONFIG_FILE = f'{CONFIGS_DIR}/config.json' DASHBOARD_QUEUE = f'{CONFIGS_DIR}/.dashboard-queue' DASHBOARD_DONE = f'{CONFIGS_DIR}/.dashboard-done' DASHBOARD_LAST_RUN = f'{CONFIGS_DIR}/.dashboard-last-run' @@ -21,31 +21,31 @@ DASHB_INTERVAL_SECS = 60 QUEUE_MAX_LINES = 50 -def load_core(): +def load_config(): try: - with open(CORE_FILE) as f: + with open(CONFIG_FILE) as f: return json.load(f) except Exception: return {} -def save_core(data): - with open(CORE_FILE, 'w') as f: +def save_config(data): + with open(CONFIG_FILE, 'w') as f: json.dump(data, f, indent=2) -def core_hash(): +def config_hash(): try: - with open(CORE_FILE, 'rb') as f: + with open(CONFIG_FILE, 'rb') as f: return hashlib.md5(f.read()).hexdigest() except Exception: return '' -def verify_core_hash(submitted): +def verify_config_hash(submitted): if not submitted: return True - return submitted == core_hash() + return submitted == config_hash() def _load_done_set(): @@ -358,7 +358,7 @@ def _items_match(item, ref): def revert_snapshot_to_core(entry_uuid): - """Apply the inverse of a snapshot to core.json and queue a new pending change. + """Apply the inverse of a snapshot to config.json and queue a new pending change. Returns (flash_message, success_bool). """ @@ -375,7 +375,7 @@ def revert_snapshot_to_core(entry_uuid): if operation == 'revert': return 'This change is already a revert; cannot revert again.', False - core = load_core() + core = load_config() if key == 'global': if before is None: @@ -396,7 +396,7 @@ def revert_snapshot_to_core(entry_uuid): items[i] = before break - msg = save_core_with_snapshot( + msg = save_config_with_snapshot( core, path=path, key=key, operation='revert', before=after, after=before, description=f"Reverted: {snap.get('description', '')}", @@ -417,7 +417,7 @@ def load_snapshot_for_uuid(entry_uuid): return None -def save_core_with_snapshot(new_core, path, key, operation, before, after, +def save_config_with_snapshot(new_core, path, key, operation, before, after, description='', cmd='core apply', queue=True): """ Write a .snapshots/{ts}-{uuid}.json file, save new_core to disk, and @@ -447,7 +447,7 @@ def save_core_with_snapshot(new_core, path, key, operation, before, after, with open(os.path.join(SNAPSHOTS_DIR, f'{entry_ts}-{entry_uuid}.json'), 'w') as f: json.dump(snapshot, f, indent=2) - save_core(new_core) + save_config(new_core) if not queue: return None diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py index 1afc1a6..71bebea 100644 --- a/docker/routlin-dash/app/view_page.py +++ b/docker/routlin-dash/app/view_page.py @@ -4,7 +4,7 @@ import json, re, subprocess, os, sys, html as html_mod import sanitize import validation as validate from datetime import datetime, timezone -from config_utils import core_hash, get_pending_entries, get_dashboard_pending, get_dashboard_done, load_snapshot_for_uuid, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, WEB_APP_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR +from config_utils import config_hash, get_pending_entries, get_dashboard_pending, get_dashboard_done, load_snapshot_for_uuid, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, WEB_APP_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR bp = Blueprint('view_page', __name__) @@ -44,8 +44,8 @@ def _load_json(path): print(f'[view_page] ERROR loading {path}: {ex}', file=sys.stderr) return {} -def _load_core(): return _load_json(f'{CONFIGS_DIR}/core.json') -def _load_ddns(): return _load_core().get('ddns', {}) +def _load_config(): return _load_json(f'{CONFIGS_DIR}/config.json') +def _load_ddns(): return _load_config().get('ddns', {}) def _load_accounts(): return _load_json(f'{DATA_DIR}/authorized_accounts.json') def _load_css(): @@ -149,17 +149,17 @@ def _iface_status(iface): return 'INVALID' -def _resolve_iface(vlan, core): +def _resolve_iface(vlan, cfg): """Compute interface name from is_vpn + derived vlan_id + general.lan_interface.""" if vlan.get('is_vpn'): - wg_vlans = [v for v in core.get('vlans', []) if v.get('is_vpn')] + wg_vlans = [v for v in cfg.get('vlans', []) if v.get('is_vpn')] wg_sorted = sorted(wg_vlans, key=lambda v: ( validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) is None, validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) or 0, )) idx = next((i for i, v in enumerate(wg_sorted) if v is vlan), 0) return f'wg{idx}' - lan = core.get('network_interfaces', {}).get('lan_interface', 'eth0') + lan = cfg.get('network_interfaces', {}).get('lan_interface', 'eth0') vid = validate.derive_vlan_id(vlan.get('subnet', ''), vlan.get('subnet_mask', 24)) or 1 return lan if vid == 1 else f'{lan}.{vid}' @@ -187,7 +187,7 @@ def _live_dhcp_leases(): def _vlan_name_for_ip(ip): import ipaddress - for vlan in _load_core().get('vlans', []): + for vlan in _load_config().get('vlans', []): subnet = vlan.get('subnet', '') mask = vlan.get('subnet_mask', 24) if not subnet: @@ -254,11 +254,11 @@ def _fmt_bytes(n): # Config data loaders =============================================== def _config_datasource(name): - core = _load_core() - vlans = core.get('vlans', []) + cfg = _load_config() + vlans = cfg.get('vlans', []) if name == 'interfaces': - gen = core.get('network_interfaces', {}) + gen = cfg.get('network_interfaces', {}) wan = gen.get('wan_interface', '') lan = gen.get('lan_interface', '') return [ @@ -267,14 +267,14 @@ def _config_datasource(name): ] if name == 'banned_ips': - return core.get('banned_ips', []) + return cfg.get('banned_ips', []) if name == 'host_overrides': - return core.get('host_overrides', []) + return cfg.get('host_overrides', []) if name == 'blocklists': rows = [] - for bl in core.get('dns_blocking', {}).get('blocklists', []): + for bl in cfg.get('dns_blocking', {}).get('blocklists', []): row = dict(bl) bl_path = f'{CONFIGS_DIR}/blocklists/{bl.get("save_as", "")}' try: @@ -288,12 +288,12 @@ def _config_datasource(name): return rows if name == 'vlans': - bl_desc = {b['name']: b.get('description', b['name']) for b in core.get('dns_blocking', {}).get('blocklists', []) if 'name' in b} + bl_desc = {b['name']: b.get('description', b['name']) for b in cfg.get('dns_blocking', {}).get('blocklists', []) if 'name' in b} rows = [] for v in sorted(vlans, key=lambda x: validate.derive_vlan_id(x.get('subnet', ''), x.get('subnet_mask', 24)) or 0): row = {k: v.get(k) for k in ('name', 'subnet', 'subnet_mask', 'radius_default', 'mdns_reflection', 'is_vpn', 'dnsmasq_log_queries')} row['vlan_id'] = validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) - row['interface'] = _resolve_iface(v, core) + row['interface'] = _resolve_iface(v, cfg) row['use_blocklists'] = json.dumps([ {'n': bl, 'd': bl_desc.get(bl, bl)} for bl in v.get('use_blocklists', []) ]) @@ -301,10 +301,10 @@ def _config_datasource(name): return rows if name == 'inter_vlan_exceptions': - return core.get('inter_vlan_exceptions', []) + return cfg.get('inter_vlan_exceptions', []) if name == 'port_forwarding': - return core.get('port_forwarding', []) + return cfg.get('port_forwarding', []) if name == 'dhcp_reservations': rows = [] @@ -418,10 +418,10 @@ def _bl_last_update(): except Exception: return '-' -def _blocklist_stats_html(core): +def _blocklist_stats_html(cfg): bl_dir = f'{CONFIGS_DIR}/blocklists' rows = '' - for bl in core.get('dns_blocking', {}).get('blocklists', []): + for bl in cfg.get('dns_blocking', {}).get('blocklists', []): name = e(bl.get('name', '')) save_as = bl.get('save_as', '') bl_path = f'{bl_dir}/{save_as}' if save_as else '' @@ -546,7 +546,7 @@ def _ddns_last_checked(): return 'Last checked: ---' def _vpn_info(): - for vlan in _load_core().get('vlans', []): + for vlan in _load_config().get('vlans', []): if 'vpn_information' in vlan: return vlan['vpn_information'] return {} @@ -556,11 +556,11 @@ def _vpn_info(): def collect_tokens(): tokens = {} - core = _load_core() - net = core.get('network_interfaces', {}) - dns_blk_gen = core.get('dns_blocking', {}).get('general', {}) - dns = core.get('upstream_dns', {}) - vlans = core.get('vlans', []) + cfg = _load_config() + net = cfg.get('network_interfaces', {}) + dns_blk_gen = cfg.get('dns_blocking', {}).get('general', {}) + dns = cfg.get('upstream_dns', {}) + vlans = cfg.get('vlans', []) tokens['GENERAL_WAN_INTERFACE'] = str(net.get('wan_interface', '-')) tokens['GENERAL_LAN_INTERFACE'] = str(net.get('lan_interface', '-')) tokens['GENERAL_WAN_STATUS'] = _iface_status(net.get('wan_interface', '')) @@ -693,10 +693,10 @@ def collect_tokens(): tokens['VPN_VLAN_COUNT'] = str(sum(1 for v in vlans if v.get('is_vpn'))) tokens['EXISTING_VLAN_IDS_JSON'] = json.dumps([validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) for v in vlans]) tokens['EXISTING_VLAN_NAMES_JSON'] = json.dumps([v.get('name', '') for v in vlans]) - tokens['EXISTING_VLAN_INTERFACES_JSON'] = json.dumps([_resolve_iface(v, core) for v in vlans]) - tokens['STAT_BANNED_IP_COUNT'] = str(sum(1 for b in core.get('banned_ips', []) if b.get('enabled', True))) - tokens['STAT_BLOCKLIST_COUNT'] = str(len(core.get('dns_blocking', {}).get('blocklists', []))) - tokens['BLOCKLIST_STATS_HTML'] = _blocklist_stats_html(core) + tokens['EXISTING_VLAN_INTERFACES_JSON'] = json.dumps([_resolve_iface(v, cfg) for v in vlans]) + tokens['STAT_BANNED_IP_COUNT'] = str(sum(1 for b in cfg.get('banned_ips', []) if b.get('enabled', True))) + tokens['STAT_BLOCKLIST_COUNT'] = str(len(cfg.get('dns_blocking', {}).get('blocklists', []))) + tokens['BLOCKLIST_STATS_HTML'] = _blocklist_stats_html(cfg) ddns = _load_ddns() ddns_gen = ddns.get('general', {}) @@ -795,7 +795,7 @@ def collect_tokens(): tokens['BLOCKLIST_NAME_OPTIONS'] = json.dumps([ {'value': bl.get('name', ''), 'label': bl.get('description', bl.get('name', ''))} - for bl in core.get('dns_blocking', {}).get('blocklists', []) + for bl in cfg.get('dns_blocking', {}).get('blocklists', []) ]) tokens['ACCOUNT_LEVEL_OPTIONS'] = json.dumps([ @@ -984,7 +984,7 @@ def _render_item(item, tokens, inherited_req=None): f'' f'' f'