From e519660ea54b0f131d3dd383a5ecd1d892d3635d Mon Sep 17 00:00:00 2001 From: Matthew Grotke Date: Wed, 27 May 2026 02:22:05 -0400 Subject: [PATCH] Development --- .../routlin-dash/app/action_networklayout.py | 30 +++++++++++++++++-- docker/routlin-dash/app/sanitize.py | 14 +++++++++ docker/routlin-dash/data/page_content.json | 3 +- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/docker/routlin-dash/app/action_networklayout.py b/docker/routlin-dash/app/action_networklayout.py index 38d09c2..371a1a6 100644 --- a/docker/routlin-dash/app/action_networklayout.py +++ b/docker/routlin-dash/app/action_networklayout.py @@ -188,13 +188,37 @@ def networklayout_tablevlans_edit(): entry['description'] = desc else: entry.pop('description', None) - hostname = identity_hostnames[i].strip() if i < len(identity_hostnames) else '' - if hostname: - entry['hostname'] = hostname + hostname_raw = identity_hostnames[i].strip() if i < len(identity_hostnames) else '' + if hostname_raw: + clean_hostname = sanitize.hostname(hostname_raw) + if clean_hostname is None: + flash(f"'{hostname_raw}' is not a valid hostname.", 'error') + return redirect(VIEW) + entry['hostname'] = clean_hostname else: entry.pop('hostname', None) new_identities.append(entry) + _ids_unchanged = ( + len(new_identities) == len(old_identities) and + all( + n.get('ip') == o.get('ip') and + n.get('description', '') == o.get('description', '') and + n.get('hostname', '') == o.get('hostname', '') + for n, o in zip(new_identities, old_identities) + ) + ) + if (name == existing.get('name', '') + and subnet == existing.get('subnet', '') + and final_mask == existing.get('subnet_mask', 24) + and dnsmasq_log_queries == bool(existing.get('dnsmasq_log_queries', False)) + and radius_default == bool(existing.get('radius_default', False)) + and mdns_reflection == bool(existing.get('mdns_reflection', False)) + and sorted(use_blocklists) == sorted(existing.get('use_blocklists', [])) + and _ids_unchanged): + flash('No changes were made.', 'info') + return redirect(VIEW) + before = {k: existing.get(k) for k in _VLAN_FIELDS} existing.update({ 'name': name, diff --git a/docker/routlin-dash/app/sanitize.py b/docker/routlin-dash/app/sanitize.py index 575073e..7e787d0 100644 --- a/docker/routlin-dash/app/sanitize.py +++ b/docker/routlin-dash/app/sanitize.py @@ -128,6 +128,20 @@ def domainname(value, max_len=253): """Hostname or domain: letters, digits, hyphens, dots. Lowercased.""" return _strip(value.lower(), r'[^a-z0-9\-.]', max_len) +_HOSTNAME_RE = re.compile(r'^[a-z0-9]([a-z0-9_\-]*[a-z0-9])?$') + +def hostname(value, max_len=253): + """Network hostname: letters, digits, hyphens, underscores. Must start and end with + alphanumeric. No consecutive hyphens or underscores. Returns lowercase if valid, None if not.""" + s = str(value).strip().lower() + if not s or len(s) > max_len: + return None + if re.search(r'[-_]{2,}', s): + return None + if not _HOSTNAME_RE.match(s): + return None + return s + def domainlist(lines): """Sanitize a list of domain name strings, returning only non-empty results.""" return [h for v in lines if (h := domainname(v))] diff --git a/docker/routlin-dash/data/page_content.json b/docker/routlin-dash/data/page_content.json index a389b13..edb49f4 100644 --- a/docker/routlin-dash/data/page_content.json +++ b/docker/routlin-dash/data/page_content.json @@ -1584,7 +1584,8 @@ "pair_col": "server_identity_descriptions", "pair_label": "Description", "pair_col2": "server_identity_hostnames", - "pair_label2": "Hostname" + "pair_label2": "Hostname", + "pair_validate2": "networkname" }, { "col": "radius_default",