Skip to content

restconf: wait for complete HTTP header before parsing#676

Merged
olofhagsand merged 1 commit into
clicon:masterfrom
hoxnox:fix-restconf-partial-header-667
Jul 4, 2026
Merged

restconf: wait for complete HTTP header before parsing#676
olofhagsand merged 1 commit into
clicon:masterfrom
hoxnox:fix-restconf-partial-header-667

Conversation

@hoxnox

@hoxnox hoxnox commented May 25, 2026

Copy link
Copy Markdown
Contributor

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.

Fixes #667

@hoxnox hoxnox force-pushed the fix-restconf-partial-header-667 branch from 190d708 to eb57d10 Compare May 25, 2026 09:42

@olofhagsand olofhagsand left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread apps/restconf/restconf_native.c Outdated
* 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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/restconf/restconf_native.c Outdated
strncmp(inbuf, "PATCH ", 6) == 0 ||
strncmp(inbuf, "DELETE ", 7) == 0;
if (looks_http && strstr(inbuf, "\r\n\r\n") == NULL){
goto ok;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should probably be a timer to avoid unbound waits on this session?

@hoxnox hoxnox force-pushed the fix-restconf-partial-header-667 branch from eb57d10 to c133150 Compare May 25, 2026 16:27
@hoxnox

hoxnox commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

I've added test and resolved all remarks.

@olofhagsand

Copy link
Copy Markdown
Member

There is a problem with nc vs netcat on linux vs musl/alpine vs freebsd, I had those problems before, see test/lib.sh

@hoxnox

hoxnox commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

corrected

There is a problem with nc vs netcat on linux vs musl/alpine vs freebsd, I had those problems before, see test/lib.sh

@hoxnox

hoxnox commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

corrected tests

@hoxnox

hoxnox commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

test timing adjusted

@olofhagsand

Copy link
Copy Markdown
Member

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.

@hoxnox

hoxnox commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

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.

@olofhagsand

Copy link
Copy Markdown
Member

My bad, I was looking at an earlier run

@hoxnox

hoxnox commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Is there anything else I need to do for that PR?

@olofhagsand

Copy link
Copy Markdown
Member

I will look at it soon

@olofhagsand olofhagsand left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider squashing the history into a single commit
(Apologies for the delay)

Comment thread apps/restconf/restconf_native.c Outdated
strncmp(inbuf, "PATCH ", 6) == 0 ||
strncmp(inbuf, "DELETE ", 7) == 0;
if (looks_http && strstr(inbuf, "\r\n\r\n") == NULL)
if (looks_http && strstr(inbuf, "\r\n\r\n") == NULL){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strstr works on null-terminated strings, but inbuf may not be, use memmem instead

Comment thread apps/restconf/restconf_native.c Outdated
}
rsock = rc->rc_socket;
clixon_debug(CLIXON_DBG_RESTCONF, "%s", rsock->rs_description?rsock->rs_description:"");
if (restconf_header_timer_unreg(rc) < 0)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May leak an open fd, place after close (or do close even if unreg fails)

@hoxnox hoxnox force-pushed the fix-restconf-partial-header-667 branch from 28eccef to f521ad4 Compare July 4, 2026 14:08
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 has not 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, located with
memmem() since the buffer is counted rather than null-terminated) 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.

The wait is bounded by a one-shot timer (RESTCONF_HEADER_TIMEOUT_S = 10s)
registered when parsing is first deferred and cancelled when the header
parses successfully or the connection is closed; on expiry the connection
is closed via restconf_close_ssl_socket().

Add test/test_restconf_partial_header.sh covering: a >2KB request split
across two writes with a delay in between (must return 200, not 400), TLS
handshake bytes on an HTTP port (must still get an immediate 400), and a
stalled partial header (must be closed by the header timeout).

Fixes clicon#667
@hoxnox

hoxnox commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

done

@olofhagsand

Copy link
Copy Markdown
Member

Thanks, I noticed memmem is non-posix, sorry, I will merge it and fix that later,

@olofhagsand olofhagsand merged commit ccccdf4 into clicon:master Jul 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RESTCONF requests with large HTTP headers are occasionally dropped

2 participants