From a5f77e4cafdfb9308fc24e457c8cbfede2716dfc Mon Sep 17 00:00:00 2001 From: Erdnusschokolade <96622762+Erdnusschokolade@users.noreply.github.com> Date: Thu, 21 May 2026 17:38:33 +0200 Subject: [PATCH 1/5] Add /vlanlist HTTP endpoint Returns a JSON array of all configured VLANs with their IDs and names, e.g. [{"id":1,"name":""},{"id":20,"name":"IoT"}]. The endpoint iterates VLAN IDs 1..4094 and filters by the validity bit in sfr_data[0] (0x02), following the same pattern as vlan_create() and vlan_setup() in rtl837x_port.c. Also adds a small itoa16_html() helper for emitting decimal numbers up to 4 digits (analogous to the existing 8-bit itoa_html()), used for VLAN IDs which can reach 4094. Response builder uses the existing vlan_name() helper for the name lookup, consistent with send_vlan(). Buffer overflow is prevented by breaking out of the iteration loop at TCP_OUTBUF_SIZE - 60. This endpoint is the foundation for upcoming UI improvements (VLAN selector dropdown and overview table). --- httpd/httpd.c | 2 ++ httpd/page_impl.c | 54 +++++++++++++++++++++++++++++++++++++++++++++++ httpd/page_impl.h | 1 + 3 files changed, 57 insertions(+) diff --git a/httpd/httpd.c b/httpd/httpd.c index 67d56d1..c05a7e1 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -682,6 +682,8 @@ void httpd_appcall(void) send_mtu(); } else if (is_word(q, "/lag.json")) { send_lag(); + } else if (is_word(q, "/vlanlist")) { + send_vlanlist(); } else if (is_word(q, "/config")) { send_config(); } else if (is_word(q, "/cmd_log")) { diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 0010063..cc1b98a 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -97,6 +97,19 @@ void itoa_html(uint8_t v) char_to_html('0' + (v % 10)); } +void itoa16_html(uint16_t v) +{ + uint8_t print_zeros = 0; + uint8_t d; + d = v / 1000; + if (d) { char_to_html('0' + d); print_zeros = 1; } + d = (v / 100) % 10; + if (d || print_zeros) { char_to_html('0' + d); print_zeros = 1; } + d = (v / 10) % 10; + if (d || print_zeros) char_to_html('0' + d); + char_to_html('0' + (v % 10)); +} + void string_to_html(__code char *s) { while (*s) char_to_html(*s++); @@ -825,3 +838,44 @@ void send_cmd_log(void) p = (p + 1) & CMD_HISTORY_MASK; } } + + +void send_vlanlist(void) +{ + /* Worst case per entry: {"id":4094,"name":""}, ~45 bytes. + * HTTP header ~50 bytes. TCP_OUTBUF_SIZE=2500 fits ~53 VLANs safely. */ + __xdata uint16_t i; + __xdata uint16_t n; + uint8_t first = 1; + + slen = strtox(outbuf, HTTP_RESPONCE_JSON); + char_to_html('['); + + for (i = 1; i < 4095; i++) { + if (vlan_get(i) < 0) + continue; + if (!(sfr_data[0] & 0x02)) + continue; + + if (!first) + char_to_html(','); + first = 0; + + slen += strtox(outbuf + slen, "{\"id\":"); + itoa16_html(i); + slen += strtox(outbuf + slen, ",\"name\":\""); + + n = vlan_name(i); + if (n != 0xffff) { + while (vlan_names[n] && vlan_names[n] != ' ') + char_to_html(vlan_names[n++]); + } + + slen += strtox(outbuf + slen, "\"}"); + + if (slen > TCP_OUTBUF_SIZE - 60) + break; + } + + char_to_html(']'); +} diff --git a/httpd/page_impl.h b/httpd/page_impl.h index 5591e14..7907285 100644 --- a/httpd/page_impl.h +++ b/httpd/page_impl.h @@ -14,6 +14,7 @@ void send_mtu(void); void send_config(void); void send_cmd_log(void); void send_lag(void); +void send_vlanlist(void); /* Convert only the lower nibble to ascii HEX char. For convenience the upper nibble is masked out. From defb37c06ab90b8d2ea295a5c2806f326d023579 Mon Sep 17 00:00:00 2001 From: Erdnusschokolade <96622762+Erdnusschokolade@users.noreply.github.com> Date: Thu, 21 May 2026 17:38:33 +0200 Subject: [PATCH 2/5] Add VLAN selector dropdown to VLAN configuration page A new + + + +
diff --git a/html/vlan.js b/html/vlan.js index 4a481e6..803db36 100644 --- a/html/vlan.js +++ b/html/vlan.js @@ -83,9 +83,40 @@ function fetchVLAN() { sendXHTTP(xhttp); } +function loadVlanList() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState !== 4) return; + var sel = document.getElementById('vlanSelect'); + if (this.status !== 200) { + sel.style.display = 'none'; + return; + } + var vlans = JSON.parse(this.responseText); + if (!vlans.length) { + sel.style.display = 'none'; + return; + } + sel.options.length = 1; + for (var i = 0; i < vlans.length; i++) { + var opt = document.createElement('option'); + opt.value = vlans[i].id; + opt.text = vlans[i].name ? vlans[i].id + ' — ' + vlans[i].name : String(vlans[i].id); + sel.appendChild(opt); + } + }; + xhttp.open('GET', '/vlanlist', true); + sendXHTTP(xhttp); +} + window.addEventListener("load", function() { update( () => { vlanForm(); + loadVlanList(); + document.getElementById('vlanSelect').onchange = function() { + document.getElementById('vid').value = this.value; + fetchVLAN(); + }; const interval = setInterval(update, 2000); }); }); diff --git a/html/vlan_sub.js b/html/vlan_sub.js index ea14c95..974dfd5 100644 --- a/html/vlan_sub.js +++ b/html/vlan_sub.js @@ -28,6 +28,7 @@ async function vlanSub() { }); console.log('Completed!', response); } + loadVlanList(); } catch(err) { console.error(`Error: ${err}`); } From a369e46fe607266e98806cb0d8a72441cfbc9031 Mon Sep 17 00:00:00 2001 From: Erdnusschokolade <96622762+Erdnusschokolade@users.noreply.github.com> Date: Thu, 21 May 2026 17:38:33 +0200 Subject: [PATCH 3/5] Add VLAN overview table to VLAN configuration page Below the configuration form, a new table lists all configured VLANs with their port memberships: Member, Tagged, Untagged, and PVID columns. Port ranges are formatted compactly (e.g. "1-2,5"). Port-to-bit mapping uses the existing physToLogPort[] array populated by /status.json, so the table is consistent with the icon view in fetchVLAN() and works on any supported machine. Each row has a delete button that sends "vlan N d" via /cmd, with a confirmation dialog. VLAN 1 cannot be deleted (no button shown). Table and dropdown both refresh automatically after Update/Create and after delete via a new refreshVlanViews() helper. The N+1 request pattern (1x /vlanlist + Nx /vlan.json) keeps the backend simple and avoids buffer overflow risks for systems with many VLANs. For typical configurations (<50 VLANs), page load remains under one second. --- html/style.css | 1 + html/vlan.html | 16 +++++++++ html/vlan.js | 91 +++++++++++++++++++++++++++++++++++++++++++++++- html/vlan_sub.js | 2 +- 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/html/style.css b/html/style.css index 581737a..84d3a2e 100644 --- a/html/style.css +++ b/html/style.css @@ -163,3 +163,4 @@ margin: 30px 0; } select { text-align-last: right; font-family: monospace} option { direction: rtl; font-family: sans-serif} +#vlanTable td { text-align: left; } diff --git a/html/vlan.html b/html/vlan.html index 353e20e..b2ffa55 100644 --- a/html/vlan.html +++ b/html/vlan.html @@ -36,6 +36,22 @@
+

Configured VLANs

+ + + + + + + + + + + + + + +
VLANNameMember PortsTagged PortsUntagged PortsPVID PortsDelete
diff --git a/html/vlan.js b/html/vlan.js index 803db36..245b7e6 100644 --- a/html/vlan.js +++ b/html/vlan.js @@ -83,6 +83,95 @@ function fetchVLAN() { sendXHTTP(xhttp); } +function portsToRange(mask, nPorts) { + var parts = []; + var start = -1, prev = -1; + for (var p = 1; p <= nPorts; p++) { + var bit = physToLogPort[p - 1]; + if ((mask >> bit) & 1) { + if (start < 0) start = p; + prev = p; + } else { + if (start >= 0) { + parts.push(start === prev ? String(start) : start + '-' + prev); + start = -1; prev = -1; + } + } + } + if (start >= 0) + parts.push(start === prev ? String(start) : start + '-' + prev); + return parts.length ? parts.join(',') : '-'; +} + +async function loadVlanTable() { + var tbody = document.getElementById('vlanTableBody'); + if (!tbody) return; + tbody.innerHTML = ''; + var resp; + try { resp = await fetch('/vlanlist'); } catch(e) { return; } + if (!resp.ok) return; + var vlans = await resp.json(); + for (var i = 0; i < vlans.length; i++) { + var v = vlans[i]; + var vresp; + try { vresp = await fetch('/vlan.json?vid=' + v.id); } catch(e) { continue; } + if (!vresp.ok) continue; + var s = await vresp.json(); + var m = parseInt(s.members, 16); + var members = m & 0x3FF; + var untag = (m >> 10) & 0x3FF; + var tagged = members & ~untag; + var pvid = parseInt(s.pvid, 16) & 0x3FF; + var tr = document.createElement('tr'); + var td, a, btn; + td = document.createElement('td'); + a = document.createElement('a'); + a.href = '#'; + a.textContent = v.id; + (function(vid) { + a.onclick = function(e) { + e.preventDefault(); + document.getElementById('vid').value = vid; + fetchVLAN(); + }; + })(v.id); + td.appendChild(a); tr.appendChild(td); + td = document.createElement('td'); + td.textContent = v.name || ''; tr.appendChild(td); + td = document.createElement('td'); + td.textContent = portsToRange(members, numPorts); tr.appendChild(td); + td = document.createElement('td'); + td.textContent = portsToRange(tagged, numPorts); tr.appendChild(td); + td = document.createElement('td'); + td.textContent = portsToRange(untag, numPorts); tr.appendChild(td); + td = document.createElement('td'); + td.textContent = portsToRange(pvid, numPorts); tr.appendChild(td); + td = document.createElement('td'); + if (v.id !== 1) { + btn = document.createElement('button'); + btn.textContent = '✕'; + (function(vid) { + btn.onclick = function() { deleteVlan(vid); }; + })(v.id); + td.appendChild(btn); + } + tr.appendChild(td); + tbody.appendChild(tr); + } +} + +function deleteVlan(id) { + if (!confirm('Delete VLAN ' + id + '?')) return; + fetch('/cmd', { method: 'POST', body: 'vlan ' + id + ' d' }) + .then(function() { refreshVlanViews(); }) + .catch(function(err) { console.error('Delete failed:', err); }); +} + +function refreshVlanViews() { + loadVlanList(); + loadVlanTable(); +} + function loadVlanList() { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { @@ -112,7 +201,7 @@ function loadVlanList() { window.addEventListener("load", function() { update( () => { vlanForm(); - loadVlanList(); + refreshVlanViews(); document.getElementById('vlanSelect').onchange = function() { document.getElementById('vid').value = this.value; fetchVLAN(); diff --git a/html/vlan_sub.js b/html/vlan_sub.js index 974dfd5..2816951 100644 --- a/html/vlan_sub.js +++ b/html/vlan_sub.js @@ -28,7 +28,7 @@ async function vlanSub() { }); console.log('Completed!', response); } - loadVlanList(); + refreshVlanViews(); } catch(err) { console.error(`Error: ${err}`); } From ce0ee859d670008ba00075c995adf22ead89ace3 Mon Sep 17 00:00:00 2001 From: Erdnusschokolade <96622762+Erdnusschokolade@users.noreply.github.com> Date: Thu, 21 May 2026 19:36:17 +0200 Subject: [PATCH 4/5] Fix: Untagged Ports column included non-member ports The hardware bitmask format encodes both "untagged members" and "non-members" in the upper 10 bits (per doc/vlan.md). The existing fetchVLAN() correctly masks this with the membership bitmask before display, but loadVlanTable() did not, causing non-member ports to appear in the Untagged Ports column. Tested on KeepLiNK KP-9000-6XH-X. --- html/vlan.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/vlan.js b/html/vlan.js index 245b7e6..d0288ca 100644 --- a/html/vlan.js +++ b/html/vlan.js @@ -119,7 +119,7 @@ async function loadVlanTable() { var s = await vresp.json(); var m = parseInt(s.members, 16); var members = m & 0x3FF; - var untag = (m >> 10) & 0x3FF; + var untag = ((m >> 10) & 0x3FF) & members; var tagged = members & ~untag; var pvid = parseInt(s.pvid, 16) & 0x3FF; var tr = document.createElement('tr'); From ba919280ac0e16f1d941226889caf2be6e535cb4 Mon Sep 17 00:00:00 2001 From: Erdnusschokolade <96622762+Erdnusschokolade@users.noreply.github.com> Date: Sun, 24 May 2026 23:31:18 +0200 Subject: [PATCH 5/5] Address PR #233 review feedback - itoa16_html: add comment that the function is sufficient for VLAN IDs (<=4094); generalization not needed. - send_vlanlist: replace post-write bounds check with a pre-write guard using worst-case entry size (138 bytes + 1 for closing bracket). Old comment claimed ~45 bytes per entry, which only held for short names. With a 117-char name (the actual bound from CMD_BUF_SIZE) entries can reach 138 bytes, and the post-check would not have prevented an overflow. - Document the 0x02 check in sfr_data[0] as the VLAN table entry valid flag. --- httpd/page_impl.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/httpd/page_impl.c b/httpd/page_impl.c index cc1b98a..66b234e 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -97,7 +97,7 @@ void itoa_html(uint8_t v) char_to_html('0' + (v % 10)); } -void itoa16_html(uint16_t v) +void itoa16_html(uint16_t v) /* sufficient for VLAN IDs (max 4094) */ { uint8_t print_zeros = 0; uint8_t d; @@ -842,8 +842,9 @@ void send_cmd_log(void) void send_vlanlist(void) { - /* Worst case per entry: {"id":4094,"name":""}, ~45 bytes. - * HTTP header ~50 bytes. TCP_OUTBUF_SIZE=2500 fits ~53 VLANs safely. */ + /* Worst case per entry: {"id":4094,"name":"<117-char name>"} = 138 bytes + * (name bound: CMD_BUF_SIZE=128 minus command prefix); +1 for closing ']'. + * At worst case ~18 VLANs fit; typical configs with short names fit many more. */ __xdata uint16_t i; __xdata uint16_t n; uint8_t first = 1; @@ -854,9 +855,12 @@ void send_vlanlist(void) for (i = 1; i < 4095; i++) { if (vlan_get(i) < 0) continue; - if (!(sfr_data[0] & 0x02)) + if (!(sfr_data[0] & 0x02)) /* bit 1: VLAN table entry valid */ continue; + if (slen + 139 > TCP_OUTBUF_SIZE) /* 138 bytes worst-case entry + 1 byte for closing ']' */ + break; + if (!first) char_to_html(','); first = 0; @@ -873,8 +877,6 @@ void send_vlanlist(void) slen += strtox(outbuf + slen, "\"}"); - if (slen > TCP_OUTBUF_SIZE - 60) - break; } char_to_html(']');