From 81af3a29a2139d542762358d748044c2e6d34124 Mon Sep 17 00:00:00 2001
From: d00f <8052722+DrDoof@users.noreply.github.com>
Date: Wed, 12 Aug 2026 20:49:44 +0200
Subject: [PATCH 1/5] httpd: return what a command printed over /cmd
POST /cmd runs the command and answers with an empty 200, so a caller cannot
tell "done" from "nothing happened", and whatever the command prints only
reaches the serial console. Reading a register over the network needs a second
channel just to see the answer.
write_char() is the one funnel every console helper goes through, and it already
tees into the syslog buffer. A third sink, live only while the httpd runs a
command that arrived over the network, puts the same bytes into the response
behind the header. Serial and syslog keep getting their copy.
The capture stops at the end of the response buffer, so a long listing is
truncated rather than overrunning it.
BANK1 grows 39 bytes and the common area 52. One byte of xdata, BANK2 unchanged.
---
httpd/httpd.c | 6 +++++-
rtlplayground.c | 9 +++++++++
2 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/httpd/httpd.c b/httpd/httpd.c
index 5e92d05b..efba40c0 100644
--- a/httpd/httpd.c
+++ b/httpd/httpd.c
@@ -21,6 +21,7 @@
extern volatile __xdata uint8_t sfr_data[4];
extern volatile __xdata uint32_t ticks;
+extern __xdata uint8_t cmd_capture; /* owned by rtlplayground.c, see write_char() */
extern __code uint8_t * __code hex;
extern __code struct f_data f_data[];
extern __code char * __code mime_strings[];
@@ -625,12 +626,15 @@ static void handle_firmware_fragment(__xdata uint8_t *p)
static void run_cmd_body(__xdata uint8_t *body)
{
+ slen = strtox(outbuf, "HTTP/1.1 200 OK\r\nConnection: close\r\n" \
+ "Content-Type: text/plain\r\n\r\n");
+ cmd_capture = 1;
execute_commands(body);
+ cmd_capture = 0;
if (err_status != ERR_OK) {
send_bad_request();
return;
}
- send_ok();
}
diff --git a/rtlplayground.c b/rtlplayground.c
index 70b8582b..aa41d9e8 100644
--- a/rtlplayground.c
+++ b/rtlplayground.c
@@ -240,6 +240,12 @@ void isr_serial(void) __interrupt(4)
}
+/* Set by the httpd while it runs a command that arrived over the network, so
+ * that everything the command prints lands in the response as well. */
+__xdata uint8_t cmd_capture;
+extern __xdata uint8_t outbuf[TCP_OUTBUF_SIZE];
+extern __xdata uint16_t slen;
+
void write_char_no_syslog(char c)
{
do {
@@ -256,6 +262,9 @@ void write_char_no_syslog(char c)
void write_char(char c)
{
+ if (cmd_capture && slen < TCP_OUTBUF_SIZE - 1)
+ outbuf[slen++] = c;
+
write_char_no_syslog(c);
if (syslog_state.enabled) {
From f46ef3a43c33ba4d53698d99853fb91806c33c3c Mon Sep 17 00:00:00 2001
From: d00f <8052722+DrDoof@users.noreply.github.com>
Date: Wed, 12 Aug 2026 22:06:33 +0200
Subject: [PATCH 2/5] system: show what the console command printed
The console tab posts to /cmd and throws the response away, so the page stayed
silent whether the command worked, was refused or was just misspelt. Now that
/cmd answers with the command's output, the page keeps a running transcript:
each command echoed after a prompt, then whatever it printed.
The box is a pre element filled through textContent, so nothing the device
prints is taken for markup. It scrolls to the newest line and the input clears
after a send.
---
html/system.html | 2 ++
html/system.js | 9 +++++++--
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/html/system.html b/html/system.html
index 9c07fdc1..f538d3ee 100644
--- a/html/system.html
+++ b/html/system.html
@@ -88,6 +88,8 @@
Be careful when entering console commands, you can lock yourself out!
+
+
diff --git a/html/system.js b/html/system.js
index f9fd37eb..0160b8b2 100644
--- a/html/system.js
+++ b/html/system.js
@@ -35,14 +35,19 @@ async function ipSub() {
}
async function cmdSub() {
- var cmd = document.getElementById('console_cmd').value;
+ const input = document.getElementById('console_cmd');
+ const out = document.getElementById('console_out');
+ const cmd = input.value;
try {
const response = await fetch('/cmd', {
method: 'POST',
body: cmd
});
- console.log('Completed!', response);
+ out.textContent += "> " + cmd + "\n" + await response.text();
+ out.scrollTop = out.scrollHeight;
+ input.value = "";
} catch(err) {
+ out.textContent += "> " + cmd + "\n" + err + "\n";
console.error(`Error: ${err}`);
}
}
From f23d840cee0aeaeeafc183fd80249983cb3602e4 Mon Sep 17 00:00:00 2001
From: d00f <8052722+DrDoof@users.noreply.github.com>
Date: Thu, 13 Aug 2026 22:42:22 +0200
Subject: [PATCH 3/5] httpd: make the /cmd answer usable when a command fails
or floods
Three ways the answer could mislead the caller, all in the paths that
matter most when something is wrong.
The capture sat in write_char(), so anything printed through
print_string_no_syslog() escaped it. That is exactly the replies of the
syslog commands, which deliberately avoid the syslog path: "syslog on"
answered with the bare address and no sentence around it, "syslog off"
answered with nothing at all. Moving the capture one level down, into
write_char_no_syslog(), catches both paths and costs nothing, since
write_char() reaches it anyway.
On a parse error send_bad_request() rewrote the buffer from the start,
throwing away the very text that explains the error and replacing it
with the 400 HTML page. Keeping the captured text and restating only the
status keeps the parser's own message; "400 NO" is as long as "200 OK",
so the header does not have to be rebuilt.
An overlong listing was cut at the buffer end with nothing to show for
it, so a clipped answer read as a short one. It now ends in a marker,
and the reserved room is accounted for in the guard.
While here: a command that configures something prints nothing, and an
empty body reads the same as "nothing ran", so a successful silent
command now answers "OK".
No internal RAM and no xdata; 174 bytes of BANK1, which has 7356 left.
---
httpd/httpd.c | 27 ++++++++++++++++++++++-----
rtl837x_common.h | 4 ++++
rtlplayground.c | 14 +++++++++++---
3 files changed, 37 insertions(+), 8 deletions(-)
diff --git a/httpd/httpd.c b/httpd/httpd.c
index efba40c0..50de43ed 100644
--- a/httpd/httpd.c
+++ b/httpd/httpd.c
@@ -16,12 +16,17 @@
#define CMARK_S 6
+/* Answer to POST /cmd. Kept as a macro so that its length is known at compile
+ * time: an unchanged slen afterwards means the command printed nothing. */
+#define CMD_RESPONSE_HEADER "HTTP/1.1 200 OK\r\nConnection: close\r\n" \
+ "Content-Type: text/plain\r\n\r\n"
+
#pragma codeseg BANK1
#pragma constseg BANK1
extern volatile __xdata uint8_t sfr_data[4];
extern volatile __xdata uint32_t ticks;
-extern __xdata uint8_t cmd_capture; /* owned by rtlplayground.c, see write_char() */
+extern __xdata uint8_t cmd_capture; /* owned by rtlplayground.c, see write_char_no_syslog() */
extern __code uint8_t * __code hex;
extern __code struct f_data f_data[];
extern __code char * __code mime_strings[];
@@ -626,14 +631,26 @@ static void handle_firmware_fragment(__xdata uint8_t *p)
static void run_cmd_body(__xdata uint8_t *body)
{
- slen = strtox(outbuf, "HTTP/1.1 200 OK\r\nConnection: close\r\n" \
- "Content-Type: text/plain\r\n\r\n");
+ slen = strtox(outbuf, CMD_RESPONSE_HEADER);
cmd_capture = 1;
execute_commands(body);
+ if (cmd_capture == 2)
+ slen += strtox(outbuf + slen, CMD_TRUNCATED);
cmd_capture = 0;
+ /* Commands that configure something print nothing at all. Saying
+ * so beats an empty body, which reads the same as "nothing ran".
+ * Only on success: a silent failure must not answer with "OK". */
+ if (err_status == ERR_OK && slen == sizeof(CMD_RESPONSE_HEADER) - 1)
+ slen += strtox(outbuf + slen, "OK\n");
+ /* On a parse error keep what the parser printed, because that text
+ * is the explanation, and only restate the status. "400 NO" is as
+ * long as "200 OK", so the header does not have to be rebuilt. */
if (err_status != ERR_OK) {
- send_bad_request();
- return;
+ outbuf[9] = '4';
+ outbuf[10] = '0';
+ outbuf[11] = '0';
+ outbuf[13] = 'N';
+ outbuf[14] = 'O';
}
}
diff --git a/rtl837x_common.h b/rtl837x_common.h
index 77a31b13..c15eced8 100644
--- a/rtl837x_common.h
+++ b/rtl837x_common.h
@@ -37,6 +37,10 @@ extern __xdata uint8_t sbuf[SBUF_SIZE];
// Size of the TCP Output buffer
#define TCP_OUTBUF_SIZE 2500
+// Appended when a captured command wrote more than the output buffer holds,
+// so a clipped listing is visibly clipped instead of silently short.
+#define CMD_TRUNCATED "\n[output truncated]\n"
+
// Size of the port name, including the terminating null byte
#define PORT_NAME_SIZE 32
diff --git a/rtlplayground.c b/rtlplayground.c
index aa41d9e8..b78cac97 100644
--- a/rtlplayground.c
+++ b/rtlplayground.c
@@ -248,6 +248,17 @@ extern __xdata uint16_t slen;
void write_char_no_syslog(char c)
{
+ /* Capturing sits here rather than in write_char() so that the messages
+ * printed through print_string_no_syslog() are captured too: those are
+ * the replies of the syslog commands, which must not generate a syslog
+ * packet but do belong in the answer to a command sent over HTTP. */
+ if (cmd_capture) {
+ if (slen < TCP_OUTBUF_SIZE - sizeof(CMD_TRUNCATED))
+ outbuf[slen++] = c;
+ else
+ cmd_capture = 2; /* out of room, httpd says so */
+ }
+
do {
} while (tx_buf_empty == 0);
if (c =='\n') {
@@ -262,9 +273,6 @@ void write_char_no_syslog(char c)
void write_char(char c)
{
- if (cmd_capture && slen < TCP_OUTBUF_SIZE - 1)
- outbuf[slen++] = c;
-
write_char_no_syslog(c);
if (syslog_state.enabled) {
From a259587bf992f7365bfc80dac93ec12d6835b708 Mon Sep 17 00:00:00 2001
From: d00f <8052722+DrDoof@users.noreply.github.com>
Date: Thu, 13 Aug 2026 22:42:32 +0200
Subject: [PATCH 4/5] system: do not let a failed console request look like an
empty answer
fetch() does not throw on 4xx, so a request that came back 401 after the
session had timed out reached the same line as a successful one: the
window showed the prompt and nothing under it, and the input was cleared
as if the command had run. The session lasts 200 seconds, so leaving the
tab open is enough to hit it.
A 401 now goes to the login page instead. The transcript also gets a
trailing newline when the answer lacks one, so the next prompt does not
run into the last line of output, and it is capped so a long session
does not keep growing the node.
---
html/system.js | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/html/system.js b/html/system.js
index 0160b8b2..c52acd03 100644
--- a/html/system.js
+++ b/html/system.js
@@ -43,7 +43,16 @@ async function cmdSub() {
method: 'POST',
body: cmd
});
- out.textContent += "> " + cmd + "\n" + await response.text();
+ if (response.status == 401) {
+ window.location.href = 'login.html';
+ return;
+ }
+ let text = await response.text();
+ if (text != "" && !text.endsWith("\n"))
+ text += "\n";
+ if (out.textContent.length > 20000)
+ out.textContent = out.textContent.slice(-16000);
+ out.textContent += "> " + cmd + "\n" + text;
out.scrollTop = out.scrollHeight;
input.value = "";
} catch(err) {
From b57b7d1a3d079bad116219027cad82b4f00b787f Mon Sep 17 00:00:00 2001
From: d00f <8052722+DrDoof@users.noreply.github.com>
Date: Thu, 3 Sep 2026 22:18:45 +0200
Subject: [PATCH 5/5] httpd: answer /cmd with the shared text header
The /cmd path carried its own copy of the plain-text response header,
which was the same string page_impl.c already keeps in
HTTP_RESPONCE_TXT, minus one difference: the shared one never said
Connection: close.
This httpd closes after every response, so a reply that does not
advertise it invites the browser to reuse the socket and lose the next
request. HTTP_RESPONCE_JSON has always said so; the text one was the
odd one out, and send_config() and send_cmd_log() were answering
without it.
The copy is gone and the shared string carries the header. The check
for a command that printed nothing now compares against the length
strtox() returned instead of sizeof() on the macro.
---
httpd/httpd.c | 11 ++++-------
httpd/page_impl.c | 2 +-
httpd/page_impl.h | 2 ++
3 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/httpd/httpd.c b/httpd/httpd.c
index 50de43ed..c5f514ad 100644
--- a/httpd/httpd.c
+++ b/httpd/httpd.c
@@ -16,11 +16,6 @@
#define CMARK_S 6
-/* Answer to POST /cmd. Kept as a macro so that its length is known at compile
- * time: an unchanged slen afterwards means the command printed nothing. */
-#define CMD_RESPONSE_HEADER "HTTP/1.1 200 OK\r\nConnection: close\r\n" \
- "Content-Type: text/plain\r\n\r\n"
-
#pragma codeseg BANK1
#pragma constseg BANK1
@@ -631,7 +626,9 @@ static void handle_firmware_fragment(__xdata uint8_t *p)
static void run_cmd_body(__xdata uint8_t *body)
{
- slen = strtox(outbuf, CMD_RESPONSE_HEADER);
+ uint16_t hdr_len = strtox(outbuf, HTTP_RESPONCE_TXT);
+
+ slen = hdr_len;
cmd_capture = 1;
execute_commands(body);
if (cmd_capture == 2)
@@ -640,7 +637,7 @@ static void run_cmd_body(__xdata uint8_t *body)
/* Commands that configure something print nothing at all. Saying
* so beats an empty body, which reads the same as "nothing ran".
* Only on success: a silent failure must not answer with "OK". */
- if (err_status == ERR_OK && slen == sizeof(CMD_RESPONSE_HEADER) - 1)
+ if (err_status == ERR_OK && slen == hdr_len)
slen += strtox(outbuf + slen, "OK\n");
/* On a parse error keep what the parser printed, because that text
* is the explanation, and only restate the status. "400 NO" is as
diff --git a/httpd/page_impl.c b/httpd/page_impl.c
index d063f4c1..a3ecd649 100644
--- a/httpd/page_impl.c
+++ b/httpd/page_impl.c
@@ -48,7 +48,7 @@ extern __xdata char sfp_module_serial[2][17];
extern __xdata uint8_t sfp_options[2];
__code uint8_t * __code HTTP_RESPONCE_JSON = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n";
-__code uint8_t * __code HTTP_RESPONCE_TXT = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n";
+__code uint8_t * __code HTTP_RESPONCE_TXT = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: text/plain\r\n\r\n";
// Convert uint8_t to ascii HEX char push on html-buffer.
void charhex_to_html(char c)
diff --git a/httpd/page_impl.h b/httpd/page_impl.h
index 31a6e08d..cad4cd6f 100644
--- a/httpd/page_impl.h
+++ b/httpd/page_impl.h
@@ -3,6 +3,8 @@
#include
+extern __code uint8_t * __code HTTP_RESPONCE_TXT;
+
bool send_counters(uint8_t phys_port);
void send_status(void);
void send_vlan(uint16_t vlan);