restconf: wait for complete HTTP header before parsing#676
Conversation
190d708 to
eb57d10
Compare
olofhagsand
left a comment
There was a problem hiding this comment.
Logic looks correct, had a minor style comment, a proposal for a timer.
Also, can one make a test for this case, ie that exercises the code?
It would mean sending a partial message, waiting, and then sending the rest, which may be non-trivial?
| * to reject with 400. Need >= 8 bytes (longest is "OPTIONS ") to | ||
| * classify; until then, assume incomplete and wait. */ | ||
| { | ||
| const char *inbuf = cbuf_get(sd->sd_inbuf); |
There was a problem hiding this comment.
Clixon convention requires all local variables declared at the top of the function, not in an inner block mid-function. The
inner {} block was added specifically to work around this. These should be hoisted to the function's variable declarations.
See CONTRIBUTING.md
| strncmp(inbuf, "PATCH ", 6) == 0 || | ||
| strncmp(inbuf, "DELETE ", 7) == 0; | ||
| if (looks_http && strstr(inbuf, "\r\n\r\n") == NULL){ | ||
| goto ok; |
There was a problem hiding this comment.
There should probably be a timer to avoid unbound waits on this session?
Large HTTP headers (e.g. JWT Bearer tokens >2KB) may be fragmented across multiple TCP segments. The current code calls clixon_http1_parse_string() on every read and, if parsing fails, uses SSL_pending() / clixon_event_poll() to decide whether more data is coming. This check is racy: when the next TCP segment hasn't yet arrived in the kernel buffer (common over WAN links with >1ms inter-packet delay), poll returns 0 and a valid request is rejected with 400 Bad Request / malformed-message. Fix this by checking for the HTTP header terminator (\r\n\r\n) before attempting to parse. If the terminator is not present, return to the event loop; the next readable event will append the remaining bytes to sd_inbuf and parsing will succeed on the complete header. To preserve immediate rejection of non-HTTP input (e.g. TLS handshake bytes sent to an HTTP port — test_restconf.sh test 15), only delay parsing when the request line begins with one of the RESTCONF-accepted methods: OPTIONS, HEAD, GET, POST, PUT, PATCH, DELETE. Anything else falls through to the parser for a proper 400. Fixes clicon#667
The partial-header wait introduced for clicon#667 lets a peer hold the socket open by sending the start of a request and then stalling. Add a one-shot timer (RESTCONF_HEADER_TIMEOUT_S = 10s) registered when we first defer parsing, cancelled when the header parses successfully or the connection is closed. On expiry, the connection is closed via restconf_close_ssl_socket(). The timer state is tracked by rc_header_timer on restconf_conn; the unreg helper is idempotent so close paths can call it unconditionally.
Exercises the HTTP/1 partial-header path that the previous commits introduced
handling for:
1. Split-write of a >2KB request (mimicking a JWT in an Authorization
header) across two writes with a sleep in between - must return 200,
not 400 malformed-message.
2. TLS handshake bytes sent to an HTTP port - must still be rejected with
400 immediately, not stall waiting for "\r\n\r\n".
3. A peer that sends a plausible request line but never completes the
header - must be closed by the header timeout (between 2s and 15s
given RESTCONF_HEADER_TIMEOUT_S = 10s).
Uses nc(1) and shell-level write+sleep to deterministically produce the
fragmented delivery; curl writes the whole request in one go and cannot
trigger the race directly.
eb57d10 to
c133150
Compare
|
I've added test and resolved all remarks. |
|
There is a problem with nc vs netcat on linux vs musl/alpine vs freebsd, I had those problems before, see test/lib.sh |
Per maintainer feedback: nc / netcat / ncat / busybox-nc differ across distros and BSDs on the -q / -N / -w flags needed for "write then close" semantics (lib.sh already documents this with an unused netcat wrapper). Switch the partial-header test to bash /dev/tcp redirections, which is already a dependency of clixon's test framework (wait_grpc in lib.sh).
|
corrected
|
bash /dev/tcp gave empty reads on Alpine CI even though the same port worked under curl. Move to expect/Tcl, which is already a clixon test dependency (see test_pagination_expect.exp) and is installed in docker/test/Dockerfile.native. Tcl's built-in 'socket' command gives us deterministic split-write semantics that no nc/netcat variant offers portably. Verified locally against a Python HTTP server that the split-write + read response loop captures the response correctly.
|
corrected tests |
On Alpine CI Test 10 reported elapsed=s (empty), even though the total script wallclock matched the 10s server header-timeout. The Tcl puts that emits the elapsed seconds runs after close $sock; on a non- blocking socket whose peer just closed, Tcl's close can raise, which terminates the script before puts reaches stdout. The captured $(...) output is then empty and the test reports failure. Time the wait in bash with date +%s and let the Tcl side just block on eof. catch around socket / read / close keeps any Tcl-level error from leaking through the heredoc exit status.
|
test timing adjusted |
|
Hey, I understand the test might be difficult to do, we could pass it if you do some ad-hoc testing for this specific thing. |
All tests is green. Everything is ready to merge. |
|
My bad, I was looking at an earlier run |
|
Is there anything else I need to do for that PR? |
|
I will look at it soon |
Large HTTP headers (e.g. JWT Bearer tokens >2KB) may be fragmented across multiple TCP segments. The current code calls clixon_http1_parse_string() on every read and, if parsing fails, uses SSL_pending() / clixon_event_poll() to decide whether more data is coming. This check is racy: when the next TCP segment hasn't yet arrived in the kernel buffer (common over WAN links with
Fix this by checking for the HTTP header terminator (\r\n\r\n) before attempting to parse. If the terminator is not present, return to the event loop; the next readable event will append the remaining bytes to sd_inbuf and parsing will succeed on the complete header.
Fixes #667