fix(picow): make the IoT gateway robust to real browsers
Two real-world failures hit the Pico W gateway that the headless e2e (node fetch, tiny request, fast timing) didn't surface: - A browser sends KILOBYTES of headers (cookies, User-Agent, sec-*). Forwarded verbatim, the request overran the chip's small recv() (e.g. recv(1024)); lwIP then RST the connection on close-with-unread- data, crashing blocking-socket sketches with ECONNRESET. Now we forward a MINIMAL request (method, path, Host, Connection: close, and Content-Type/Length for bodies) — nothing a tiny server can choke on. - After a gateway request completed we dropped the connection immediately, so a late chip segment (retransmitted FIN / trailing ACK) no longer matched and fell through to the chip-initiated NAT, which RST it. Add a short TIME_WAIT: keep the connection briefly and re-ACK late segments so the chip closes cleanly, never RSTing it.
This commit is contained in:
parent
319decf1bd
commit
bb4d06cc7a
|
|
@ -133,11 +133,6 @@ async def _proxy_esp32(inst, path: str, request: Request) -> Response:
|
|||
)
|
||||
|
||||
|
||||
# Hop-by-hop / per-connection headers we never forward verbatim.
|
||||
_HOP_BY_HOP = {'host', 'transfer-encoding', 'connection', 'content-encoding',
|
||||
'keep-alive', 'proxy-connection', 'upgrade'}
|
||||
|
||||
|
||||
async def _proxy_picow(bridge, path: str, request: Request) -> Response:
|
||||
"""Reverse-proxy to a Pico W web server living in the browser-side lwIP.
|
||||
|
||||
|
|
@ -149,13 +144,17 @@ async def _proxy_picow(bridge, path: str, request: Request) -> Response:
|
|||
query = request.url.query
|
||||
target = '/' + path + (('?' + query) if query else '')
|
||||
|
||||
headers = {
|
||||
k: v for k, v in request.headers.items()
|
||||
if k.lower() not in _HOP_BY_HOP
|
||||
}
|
||||
headers['Host'] = STA_IP
|
||||
headers['Connection'] = 'close' # make the chip's server FIN when done
|
||||
if body and 'content-length' not in {k.lower() for k in headers}:
|
||||
# Forward a MINIMAL request. The chip's servers are tiny — they typically
|
||||
# read one small recv() (e.g. recv(1024)) and don't parse beyond the
|
||||
# request line + Host. A browser sends kilobytes of headers (cookies,
|
||||
# User-Agent, sec-*, ...); forwarding them verbatim overruns that recv,
|
||||
# and the chip's lwIP then RSTs the connection when it closes with unread
|
||||
# data — crashing blocking-socket sketches with ECONNRESET. Keep it lean.
|
||||
headers = {'Host': STA_IP, 'Connection': 'close'}
|
||||
ctype = request.headers.get('content-type')
|
||||
if ctype:
|
||||
headers['Content-Type'] = ctype
|
||||
if body:
|
||||
headers['Content-Length'] = str(len(body))
|
||||
|
||||
req_line = f'{request.method} {target} HTTP/1.1\r\n'
|
||||
|
|
|
|||
|
|
@ -128,6 +128,10 @@ class TcpInbound:
|
|||
return
|
||||
|
||||
if conn.state != _State.ESTABLISHED:
|
||||
# TIME_WAIT (or pre-handshake): re-ACK late retransmits so the chip
|
||||
# closes cleanly. Never let it reach the outbound NAT (RST).
|
||||
if tcp.payload or (tcp.flags & (TCP_FIN | TCP_SYN)):
|
||||
await self._send(conn, TCP_ACK)
|
||||
return
|
||||
|
||||
# In-order data only; re-ACK and drop anything out of order so the
|
||||
|
|
@ -199,7 +203,14 @@ class TcpInbound:
|
|||
break # got a full header block and went idle — good enough
|
||||
return bytes(conn.rx) if conn.rx else None
|
||||
finally:
|
||||
self._conns.pop(our_port, None)
|
||||
# TIME_WAIT: keep the connection around briefly so late chip
|
||||
# segments (a retransmitted FIN, a trailing ACK) still match this
|
||||
# connection and get absorbed/re-ACKed here, instead of falling
|
||||
# through to the chip-initiated NAT which would RST them — a RST
|
||||
# crashes blocking-socket sketches with ECONNRESET.
|
||||
conn.state = _State.CLOSED
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.call_later(5.0, self._conns.pop, our_port, None)
|
||||
|
||||
# ── frame emission ─────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue