edc469f made max_upload count, which bounds how many bytes a request may
carry. Nothing bounds where those bytes land. The two are not the same
check: the first watches the request, the second watches the flash address.
uptr walks forward one page at a time from FIRMWARE_UPLOAD_START and has no
ceiling. A body whose closing boundary never arrives keeps writing page after
page past the staging region, and on a flash exactly twice FIRMWARE_UPLOAD_START
the address wraps onto the running image at zero, so a bad upload takes the
working firmware with it. That commit's own message notes the related hazard:
flash_write_bytes() carries the address in three bytes and drops what does not
fit.
Bound it where uptr actually moves: refuse the next page once it would leave
the area check_and_flash_update_image() reads back, and answer 400. A partial
image left behind is harmless - boot rejects it on CRC and erases it.
This rebase drops the part of the original patch that removed max_upload;
the length limit and the address limit now sit side by side.
Building with SDCC 4.6.0 emits tons of warnings e.g.:
cmd_parser.c:31: warning 356: object in read-only code space should be const
cmd_parser.c:36: warning 357: pointer to object in read-only code space should be pointer to const
These of course become fatal when building with --Werror.
Comparing resultant binaries, it seems that a single string is shifted
(deduplication?) but the numbers emitted at the end of the build are
otherwise identical.
max_upload was armed once when the upload began and never reduced, so
`max_upload - uip_len > 0` was an unsigned expression that only went
false when a segment arrived whose length matched the limit exactly. The
write pointer climbed from the staging address with nothing to stop it,
while flash_write_bytes() carries the address in three bytes and drops
whatever does not fit.
Subtract each segment as it is taken and compare before subtracting, which
is what the comment there described. The limit was only ever armed for
firmware uploads; configuration uploads keep the bound their own buffer
already imposes.
The multipart branch for /config tests authenticated before it touches
anything; the branch for /upload does not, and the 401 for it is only
raised further down at the dispatch. By then the request has already set
config_upload, uptr, verify_crc, max_upload and pre_acc, and announced a
firmware upload on the console.
Those five are globals while the transfer state they steer is per
connection, and the stack is built for ten connections, so a request that
is about to be rejected can still retarget an upload another client is in
the middle of: a configuration upload continues as a firmware one and its
remaining parts are written to the staging area instead of the
configuration sector.
Apply the same test the neighbouring branch uses, before the assignments.
gcc flags the else whose only statement is dbg_string(), because that macro
expands to nothing unless DEBUG is on, leaving the branch with an empty body.
The line is older than this branch; the bench is simply the first thing that
compiles httpd.c with a host compiler and -Wall -Wextra.
Turn warnings into errors in the test Makefile too, so the next one is caught
where it appears instead of scrolling past. It is the only warning the four
test binaries produce, so nothing else has to change.
uip_mss() is the limit on the next segment, not a record of the last
one: uIP recomputes it from the window the peer advertises in every
incoming segment, before the application is called. The ACK path
subtracted it from slen and added it to o_idx, and the retransmit path
handed it to uip_send(), so whenever the window moved between a segment
going out and its ACK coming back, the file being served lost or
repeated the difference. A window shrinking from 1460 to 600 between the
two repeats 860 bytes under fresh sequence numbers, which a browser
writes into the file; a window opening the same way skips 860.
Remember the length in tx_send() and use it in both places. The
retransmit path then repeats exactly the segment uIP still counts as
outstanding, which is what uip_connr->len makes it put on the wire.
uip_appdata is void *, so p - uip_appdata subtracts pointers of
different types. SDCC accepts it, C does not, and no host compiler will
build the file for a unit test. Cast to the type the byte count is meant
in; the generated firmware code is unchanged.
The vendor, model and serial strings are filtered while the EEPROM read
fills the buffers, so the page copies them out as they are. NUL still
ends a string, so a field shorter than its slot reads the same as before.
The three loops in send_status() become one call each. strcpy() cannot
return the length: rtl837x_common.h hides that prototype under
RTLP_HOST_TEST so host builds take the C library version, and a call
that adds its result to slen does not compile there. A small static
helper does the copy instead, and bank 1 drops by 96 bytes.
A port named NAS"1 made status.json unparsable, so the page stopped
loading, and the name survives a reboot in the saved config. VLAN names
and the SFP strings have the same problem.
They now go through a helper that replaces ", \ and non-printable bytes
with a dot, the way the hostname is already sanitized on ingest. The
length does not change, so the size checks still hold.
The helper also covers the 0xa0 byte that send_sfp_info() skipped as a
special case, so that check is gone.
Compiling these two files for the host turns up warnings that SDCC does
not print: four places mix & with | and two mix && with ||, and l2_delete
keeps a counter it never reads. The parentheses only say what the
compiler already does, so the generated code is unchanged.
counter_to_html() reads the SFRs directly for the zero test, which saves
14 bytes of bank 1.
send_status wrote the high and the low word of a packet counter one after
the other, each without its leading zeros. That reads fine until a port
passes four billion packets; from then on a low word of 5 prints as "0x15"
next to a high word of 1, and the rates the page derives from the
difference between two readings jump around. send_counters already padded
the low word, so both now share one helper that pads it only when the high
word is set, which also drops the leading "0" the counters page used to
print in front of every small value.
handle_post() takes the request path from byte 6 of the segment it is
given. For the first segment of a POST that is the path, but for a later
segment of a multipart upload it is whatever part of the file happens to
sit there. The cmd and login handlers were matched before the multipart
state was checked, so a segment of a config or firmware upload with
"cmd " at that offset ran the rest of the segment as CLI commands, and
one with "login " there aborted the upload with 400.
Check for an upload in progress, and for new upload and config requests,
before looking at cmd and login. A new POST cannot match both sets of
words, so nothing changes for requests that are not uploads.
session_id is cleared at boot and only a successful login fills it in,
while the uptime counter and last_session_use both start at 0. For the
first SESSION_TIMEOUT seconds after a start, a request carrying an empty
"Cookie: session=" therefore passes is_word_x(session, session_id) and
is treated as logged in, which is enough for /cmd, /config and /upload.
Only look at the cookie once a session has been handed out.
The pages become one index.html holding every section, one app.js and a
self-contained login.html. The layout is a sidebar with light, dark and
Selenized themes (or follow the OS), a live dashboard with per-port
packet rates, and confirmation dialogs in front of the actions that can
lock the operator out.
Behaviour changes against the previous pages:
- every request goes through one queue that waits for the previous
response BODY rather than its headers: the firmware shares a single
output buffer across connections and delimits responses by closing,
so a second request in flight corrupts the first
- "Save to flash" merges the command log into the startup configuration
with the grammar the console accepts, shows the result before writing,
verifies the sector by reading it back and clears the command log only
after that; the header shows an "unsaved changes" badge while the
running configuration differs from flash
- the console section shows the output the firmware returns for a
command, including the parser's explanation on a rejected one
- the firmware update section checks size, bank header and CRC16 of the
image in the browser before uploading, shows upload progress and waits
for the switch to come back after the reset
- the ports table shows the learned MAC of a port when there is exactly
one and the count otherwise; the MAC table sorts per column, filters,
and names the LAG a learned address sits behind
- the spanning tree section sends only the settings that changed on
Apply instead of one command per control event
- link aggregation groups are numbered 1-4 like the command line, and
applying an empty group clears it
The translation table is inline in app.js with English, Japanese and
Chinese. The language selector sits in the page header and reloads the
page so that dynamically built tables are rebuilt in the new language.
Only login.html and the favicon are served before authentication; the
stylesheet, the port images and the old script bundle no longer exist.
The simulator's allowlist follows the firmware's.
The session timer was refreshed only when a page from the file table was
served, so a client that loads its pages once and then talks to the JSON
endpoints alone is logged out SESSION_TIMEOUT seconds after the load,
however busy it is. A firmware upload started after that point is answered
with 401 instead of a verdict. Refresh the timer wherever the session
cookie validates, which covers every authenticated request, and drop the
file-serving refresh that this makes redundant.
Spanning tree so far saw only physical ports. Two ports aggregated towards
the same neighbour look to the protocol like two parallel paths, so it
blocks one of them and cuts the group in half.
Give each of the four groups an STP entity of its own, numbered above the
physical ports. A member port answers to its group: BPDUs arriving on it
are accounted to the entity, the entity's state is written to every member
together, and its BPDUs carry the state of a live member. Ports outside
any group keep behaving exactly as before.
Membership comes from the aggregation tables once a second; when it
changed, the entity's state is reapplied to the new set of members. The
CLI takes lag <1-4> wherever it takes port <n>, the web UI shows a group
as one row, and the saved config keeps the lag form of the commands.
The lookup tables live in BANK1, so the routine switches the code bank to
reach them with movc and never switches it back. That is the contract the
__naked declaration already implied: same bank or common segment only.
The name now says it.
crc16_bank1 is the routine unchanged. crc16 becomes a __banked wrapper in
BANK1, so a caller from another bank has its own bank put back by
__sdcc_banked_ret. The two existing call sites take the direct path and
pay nothing.
A file larger than the output buffer is sent in two phases: the first
buffer goes out in MSS-sized pieces, with o_idx walking through it, and
what is left is then streamed from flash a chunk at a time into outbuf
from offset 0.
The continuation branch never reset o_idx, so it kept the offset the first
phase had left it at, while uip_rexmit() retransmits from outbuf + o_idx.
A lost continuation segment was therefore retransmitted as the bytes 1460
further into the buffer, which the client accepts as the missing data, and
with o_idx + slen past the 2500-byte buffer it also read whatever xdata
follows outbuf.
Reproduced on a SWTGW218AS by dropping 15% of the switch's TCP segments:
seven of eight fetches of a 21 kB static came back the right length and
the wrong content, each first differing at a fixed phase of the 1460-byte
chunk. With o_idx reset the retransmission repeats the chunk it sent.
The o_idx += slen right after slen = 0 went too: it added nothing.
(cherry picked from commit 789d29423288f89411a41673e26ce1854b296c3d)
uip_init() does not set uip_conn, and httpd_init() runs right after it, so
the assignment went through a pointer nothing had assigned yet.
It has stayed invisible because the startup code does clear xdata: uip_conn
is NULL rather than garbage, appstate sits 28 bytes into struct uip_conn, and
the byte therefore lands at xdata 0x1C. With the current map that is inside
the command line buffer, at an offset nothing reads while the line is empty,
and cmd_editor_init() runs later anyway.
Nothing is broken today, but it is a write to an address that has nothing to
do with the state being set, and it moves the moment the struct grows or the
map shifts. Address the connection directly.
A device on an aggregate is learned against whichever member the frame came in
on, and the hash moves that around: the same address changes port every time
the flow mix changes. The table was telling the truth and there was no way to
read it, since nothing on the page or the console says those two ports are one
link.
port_lag_of() already exists and already knows the answer, so both views ask
it. The console prints LAG1 in place of the member, and /l2.json carries a
"lag" field beside the port, 0 when the port stands alone, which is the shape
/stp.json already uses.
Costs 151 bytes of BANK1.
* uip: time out an ESTABLISHED connection whose peer went silent
uIP has no keepalive and no idle timeout for an ESTABLISHED connection, so
a peer that vanishes without FIN or RST holds it open forever. A crashed
client does it, and so does a SYN caught while the switch is still
booting. With UIP_CONF_MAX_CONNECTIONS = 1 that wedges the single slot:
the web UI stops answering, port 80 no longer even SYN-ACKs, and only a
power cycle recovers it. Every other TCP state already has an exit path
(UIP_MAXRTX, UIP_TIME_WAIT_TIMEOUT); idle ESTABLISHED was the one hole.
Close it inside uIP rather than in httpd, so any future TCP service is
covered too. The retransmission timer is unused while nothing is in
flight, so the idle count lives in it: a 1 Hz strobe derived from the
uip_periodic() rate ages it, any arriving segment clears it, and past the
limit the connection is aborted through the same path as UIP_MAXRTX. Two
new options in uipopt.h, default off; this project enables 30 s.
httpd also joins uip_timedout() to its cleanup: it only handled closed
and aborted, so both this timeout and the existing UIP_MAXRTX one would
have left the transfer state stuck on a dead connection.
* uip: keep the tuning values out of the prose
The comment named the timeout and the tick rate, which go stale the
moment either constant is edited. It now says why an idle connection
is aged out and leaves the numbers to the defines under it.
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.
Test the busy flag and compare the entry's MAC against the SFR data
registers directly instead of going through the sfr_data copy, and
rewrite TBL_DATA_0 in place rather than reassembling all four bytes.
Smaller and faster on the deepest httpd path.
Suggested-by: vDorst
The L2 page's Delete button removes any table entry by index, including
the entry for the switch's own MAC on the CPU port. Without that entry
unicast to the management address is no longer delivered to the CPU, so
management goes dark until the CPU happens to transmit again and the
entry is relearned. Compare the entry's MAC against uip_ethaddr before
rewriting it and answer result 0 for the switch's own address, as for a
missing entry. The compare is inline: this handler sits at the bottom of
the uIP call chain, where every extra call level costs internal RAM.
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.
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.
login.html asks for i18n.js, but the file is not on the short list a
client may have before it authenticates, so the request is answered with
a redirect back to the login page and the strings stay as the English
that is baked into the markup.
The file carries only the interface text, the same text the page already
ships untranslated, so serving it before login gives nothing away.
The enabled flag is a flag, so say bool. The header length no longer needs
to live in xdata: it was put there when a local would not link, and there
is room for it here.
The overview names the row after the JSON key, so the syslog row said
"syslog_server_ip" while carrying only an address. It now says
"syslog_server" and carries the port with it.
A POST /cmd whose body arrives in a separate TCP segment from the headers
executed nothing: handle_post() read the body from the segment that
carried the request line, found it empty, and answered 200 OK. Python's
urllib and requests both write headers and body separately, so every
scripted command from those clients became a silent no-op. POST /login had
the same hole. The multipart endpoints were fixed earlier; this covers the
plain ones.
When the first segment holds fewer body bytes than Content-Length
announces, the bytes accumulate in config_buf (idle outside multipart
uploads) and the connection waits in TSTATE_POSTBODY; once the announced
length is in, the endpoint runs against the buffer. The /cmd and /login
executions move into run_cmd_body() and run_login_body() so both paths
share them; the 200 OK builder they duplicated becomes send_ok(), and
run_login_body() checks the pwd= prefix instead of skipping four bytes
blindly.
The announced length is the contract on both paths. A body that arrives
with the headers is terminated at Content-Length before it runs, so
pipelined bytes after it are not executed as commands the way the old
code did. A request without a usable Content-Length answers 411: without
one there is no way to know when the body has arrived, and the old 200
OK for an empty body is the bug this fixes. Bodies announced at or above
the buffer size answer 400 before anything is buffered.
The wait state has a deadline. uIP is built with a single connection,
and an ESTABLISHED connection with nothing in flight never times out on
its own, so headers followed by silence (a script interrupted, a link
dropped mid-request, or someone holding the socket on purpose) would
otherwise keep the slot until reboot; POST /login reaches the wait
before authentication. The poll handler aborts the connection when the
body has made no progress for five seconds, measured on the 200 Hz
system tick rather than on poll count, which runs faster under interrupt
load.
Record the announced body length alongside Content-Type and the session
cookie. The value goes through parse_short(), so an announcement that does
not fit sixteen bits saturates at 0xffff rather than wrapping to something
the size checks would accept, and a missing or unparseable header reads as 0.
Nothing consumes it yet; the plain POST endpoints take it up next.
The scan stepped over the NUL that ends the received data before breaking,
so on a request whose headers were cut short it returned a pointer one past
the data. handle_post() then tested that byte for the end of the header,
reading whatever an earlier packet had left there instead of the NUL the
appcall wrote, and could go on to run an endpoint against the stale bytes.
Break before advancing so the returned pointer sits on the NUL and the
caller's check sees it.
The digit loop accumulated into a uint16_t without a bound, so a query
such as /vlan.json?vid=65540 read as VLAN 4 and every consumer saw a small,
valid-looking number for an out-of-range one. Clamp the result at 0xffff
once another digit would overflow (6552 * 10 + 9 is the last value that
fits). The consumers already reject or mask 0xffff: vlan_get() refuses
anything from 4095 up, send_l2() masks the index to the table size, and
l2_delete() masks the high byte.
scan_header() found Content-Type and Cookie with is_word(), which compares
bytes exactly and requires a separator after the pattern. Field names are
case-insensitive (RFC 7230 section 3.2), and the whitespace after the colon
is optional, so "content-type: multipart/..." and "Content-Type:multipart/..."
were both treated as absent. The value pointers were then fixed offsets that
assumed exactly one space.
Add header_value(), which matches a lower-case name anchored at the line
start, folds the request bytes to lower case as it compares, and returns
the start of the value past any blanks, so a call site no longer adds the
name length by hand. Cookie scanning reuses the returned pointer, and the
end-of-header test becomes the strstart() the file already has.