From 275ccd0bac6fdcba12f9df635a02962131c74f09 Mon Sep 17 00:00:00 2001 From: Matthew Grotke Date: Sun, 24 May 2026 00:08:14 -0400 Subject: [PATCH] Development --- docker/routlin-dash/app/view_page.py | 64 ++++++++++++++-------------- routlin/ddns.py | 10 ++++- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py index 9c1575b..dab25e1 100644 --- a/docker/routlin-dash/app/view_page.py +++ b/docker/routlin-dash/app/view_page.py @@ -325,10 +325,12 @@ def _config_datasource(name): row = dict(p) ptype = p.get('provider', '').lower() if ptype == 'noip': - row['credentials'] = f'U: {e(p.get("username", "-"))}
P: •••' + row['credentials'] = (f'
' + f'U: {e(p.get("username", "-"))}
' + f'P: •••
') elif ptype in ('cloudflare', 'duckdns'): tok = p.get('api_token', '') - row['credentials'] = f'API Token: {tok[:8]}...' if tok else '(not set)' + row['credentials'] = f'API Token: {e(tok[:16])}...' if tok else '(not set)' else: row['credentials'] = '-' row['hostnames'] = json.dumps(p.get('hostnames', p.get('subdomains', []))) @@ -628,7 +630,7 @@ def collect_tokens(): f'{rows}' '' '
' - f'' + f'' '
' '' '
' @@ -915,13 +917,13 @@ def _render_item(item, tokens, inherited_req=None): f'
' f'' f'
' f'' f'
' - f'
' + f'' f'' f'' ) @@ -943,15 +945,15 @@ def _render_item(item, tokens, inherited_req=None): action = e(apply_tokens(item.get('action', ''), tokens)) method = e(item.get('method', 'post')) inner = render_items(item.get('items', []), tokens, req) - hash_field = f'' + hash_field = f'' originals = json.dumps(_collect_form_originals(item.get('items', []), tokens)) - orig_field = f'' + orig_field = f'' return f'
{hash_field}{orig_field}{inner}
' if t == 'hidden': name = e(item.get('name', '')) value = e(apply_tokens(item.get('value', ''), tokens)) - return f'' + return f'' if t == 'field': return _render_field(item, tokens) @@ -978,9 +980,9 @@ def _render_item(item, tokens, inherited_req=None): f'
' f'' f'
' - f'' + f'' f'/' - f'' + f'' f'{e(dotted)}' f'
' f'' @@ -1022,7 +1024,7 @@ def _render_field(item, tokens): readonly = ' readonly' if item.get('readonly') else '' if input_type == 'hidden': - return f'' + return f'' if input_type == 'checkbox': checked = 'checked' if value.lower() in ('true', '1', 'yes') else '' @@ -1031,12 +1033,12 @@ def _render_field(item, tokens): return (f'
' f'' f'{hint_html}
') return (f'
' f'{hint_html}
') if input_type == 'checkbox_group': @@ -1048,7 +1050,7 @@ def _render_field(item, tokens): boxes = ''.join( f'' for o in opts ) @@ -1075,7 +1077,7 @@ def _render_field(item, tokens): min_attr = f' min="{item["min"]}"' if 'min' in item else '' max_attr = f' max="{item["max"]}"' if 'max' in item else '' return (f'
' - f'' + f'' f'{hint_html}
') if input_type == 'textarea': @@ -1175,7 +1177,7 @@ def _render_field(item, tokens): return (f'
' f'' f'
' - f'' + f'' f'
' f'' f'{ext_meta}' @@ -1190,7 +1192,7 @@ def _render_field(item, tokens): dyn_hint = '' if (item.get('readonly') or item.get('dyn_hint') or validate) else '' return (f'
' f'{hint_html}{dyn_hint}
') + f' placeholder="{placeholder}" class="form-input{extra_cls}"{readonly}{validate_attr}/>{hint_html}{dyn_hint}
') def _collect_form_originals(items, tokens): @@ -1249,7 +1251,7 @@ def _render_editable_list(item, tokens): rows = ''.join( f'
' - f'' + f'' f'' f'
' for v in items_list @@ -1323,8 +1325,8 @@ def _render_table(item, tokens, inherited_req=None): btns += f'' continue btns += (f'
' - f'' - f'' + f'' + f'' f'
') elif method == 'js_edit': target = e(ra.get('target', 'edit-form')) @@ -1377,7 +1379,7 @@ def _render_table_cell(value, render_fn, col_class='', field='', row_idx=None, label = 'Disabled'; badge_cls = 'badge-disabled' if toggle_action and row_idx is not None and toggle_allowed: inner = (f'
' - f'' + f'' f'
') else: @@ -1497,8 +1499,8 @@ def render_layout(view_id, content_html, tokens): pass return (f'\n\n\n' - f' \n' - f' \n' + f' \n' + f' \n' f' {PRODUCT_DISPLAY_NAME}\n' f' \n' f'\n\n' @@ -1950,13 +1952,13 @@ document.addEventListener('click', function(e) { if (provider === 'noip') { return '
U:' + '
' + + '" class="form-input inline-edit-input"/>
' + '
P:' + '
'; + '" class="form-input inline-edit-input"/>
'; } else { return ''; + '" class="form-input inline-edit-input" placeholder="API Token"/>'; } } @@ -1971,7 +1973,7 @@ document.addEventListener('click', function(e) { if (inputType === 'checkbox') { var checked = (val === true || val === 'true' || val === 1 || val === '1'); td.innerHTML = ''; + (checked ? ' checked' : '') + ' class="inline-edit-checkbox"/>'; } else if (inputType === 'checkbox_multi') { var opts = fDef.options || []; var checked = []; @@ -1981,7 +1983,7 @@ document.addEventListener('click', function(e) { var isChecked = checked.indexOf(o.value) !== -1; cbHtml += ''; + (isChecked ? ' checked' : '') + ' class="inline-edit-checkbox-multi"/> ' + esc(o.label) + ''; }); cbHtml += '
'; td.innerHTML = cbHtml; @@ -1998,7 +2000,7 @@ document.addEventListener('click', function(e) { var minAttr = fDef.min !== undefined ? ' min="' + esc(String(fDef.min)) + '"' : ''; var maxAttr = fDef.max !== undefined ? ' max="' + esc(String(fDef.max)) + '"' : ''; td.innerHTML = ''; + '"' + minAttr + maxAttr + ' class="form-input inline-edit-input"/>'; } else if (inputType === 'textarea') { var textVal; try { var arr = JSON.parse(val); textVal = Array.isArray(arr) ? arr.join('\n') : String(val||''); } @@ -2011,7 +2013,7 @@ document.addEventListener('click', function(e) { var validateAttr = fDef.validate ? ' data-validate="' + esc(fDef.validate) + '"' : ''; var hintHtml = fDef.validate ? '' : ''; td.innerHTML = '' + hintHtml; + '" value="' + esc(String(val)) + '" class="form-input inline-edit-input"' + validateAttr + '/>' + hintHtml; if (fDef.validate && typeof validateEl === 'function') validateEl(td.querySelector('input')); } }); diff --git a/routlin/ddns.py b/routlin/ddns.py index 8fcc60a..b1b6c01 100644 --- a/routlin/ddns.py +++ b/routlin/ddns.py @@ -210,11 +210,17 @@ def _get_ip_via_http(spec): return _extract_ip(r.read().decode().strip()) +_SAFE_DIG_RE = re.compile(r'^[a-zA-Z0-9.\-_@+:\s]+$') + def _get_ip_via_dig(spec): - """Query public IP via dig. spec: {"type": "dig", "command": ""} + """Query public IP via dig. spec: {"type": "dig", "url": ""} Requires the 'dig' utility to be installed. """ - cmd = ["dig", "+short"] + spec["url"].split() + url = spec["url"] + if not _SAFE_DIG_RE.match(url): + log.warning(f"Skipping dig service with disallowed characters: {url!r}") + return None + cmd = ["dig", "+short"] + url.split() try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) if result.returncode != 0: