diff --git a/lib/roles/h2/hpack.c b/lib/roles/h2/hpack.c index fbb6294ab..0a8e628cb 100644 --- a/lib/roles/h2/hpack.c +++ b/lib/roles/h2/hpack.c @@ -1070,6 +1070,27 @@ int lws_hpack_interpret(struct lws *wsi, unsigned char c) ah->parser_state = WSI_TOKEN_NAME_PART; ah->lextable_pos = 0; h2n->unknown_header = 0; +#if defined(LWS_WITH_CUSTOM_HEADERS) + /* + * h2 doesn't use the h1 header text parser to lay down + * the unknown-header storage (the name comes in via + * hpack one char at a time and the value never passes + * through the h1 value collector). So for substreams + * we build the UHO entry ourselves as the name/value + * are decoded: speculatively reserve the entry header + * before the name now, then finalize (or abandon, if it + * turns out to be a header lws knows) at name complete. + */ + if (wsi->mux_substream && !h2n->value) { + ah->unk_pos = 0; + if (ah->pos + UHO_NAME < + wsi->a.context->max_http_header_data) { + ah->unk_pos = ah->pos; + for (n = 0; n < UHO_NAME; n++) + ah->data[ah->pos++] = 0; + } + } +#endif break; } @@ -1181,8 +1202,14 @@ int lws_hpack_interpret(struct lws *wsi, unsigned char c) __func__); return 1; } - } //else - //lwsl_header("ignoring %c\n", c1); + } +#if defined(LWS_WITH_CUSTOM_HEADERS) + else if (wsi->mux_substream && ah->unk_pos && + ah->unk_value_pos && ah->pos + 1 < + wsi->a.context->max_http_header_data) + /* collect unknown-header value byte */ + ah->data[ah->pos++] = (char)c1; +#endif } else { /* * Convert name using existing parser, @@ -1206,6 +1233,19 @@ int lws_hpack_interpret(struct lws *wsi, unsigned char c) "Uppercase literal hpack hdr"); return 1; } +#if defined(LWS_WITH_CUSTOM_HEADERS) + /* + * Speculatively collect the name into the UHO + * entry we reserved. We must do this before + * lws_parse() below: if it recognizes a known + * header it does lws_frag_start() using the + * current ah->pos, which has to already point + * past the name we are stashing here. + */ + if (ah->unk_pos && + ah->pos + 1 < wsi->a.context->max_http_header_data) + ah->data[ah->pos++] = (char)c1; +#endif plen = 1; if (!h2n->unknown_header && lws_parse(wsi, &c1, &plen)) @@ -1259,6 +1299,38 @@ int lws_hpack_interpret(struct lws *wsi, unsigned char c) } } +#if defined(LWS_WITH_CUSTOM_HEADERS) + /* + * The name part just completed. If lws didn't recognize it, + * finalize the UHO entry we have been building (the name is + * stored with a trailing ':' to match the h1 storage format, + * which is what the lws_hdr_custom_*() accessors expect) and + * link it into the unknown-header list. Otherwise drop the + * speculative name (the recognized header's value frag was + * already located past it by the lws header parser). + */ + if (wsi->mux_substream && ah->unk_pos && !h2n->value) { + if (h2n->unknown_header && + ah->pos + 1 < wsi->a.context->max_http_header_data) { + ah->data[ah->pos++] = ':'; + lws_ser_wu16be((uint8_t *)&ah->data[ah->unk_pos + + UHO_NLEN], + (uint16_t)((ah->pos - ah->unk_pos) - + UHO_NAME)); + if (!ah->unk_ll_head) + ah->unk_ll_head = ah->unk_pos; + if (ah->unk_ll_tail) + lws_ser_wu32be((uint8_t *)&ah->data[ + ah->unk_ll_tail + UHO_LL], + ah->unk_pos); + ah->unk_ll_tail = ah->unk_pos; + ah->unk_value_pos = ah->pos; + } else + /* known header, or no room: drop capture */ + ah->unk_pos = 0; + } +#endif + /* we have the header */ if (!h2n->value) { h2n->value = 1; @@ -1355,6 +1427,20 @@ int lws_hpack_interpret(struct lws *wsi, unsigned char c) if (lws_hpack_handle_pseudo_rules(nwsi, wsi, m)) return 1; +#if defined(LWS_WITH_CUSTOM_HEADERS) + /* + * Value part complete: if we were collecting an unknown + * header, record the value length to finish the UHO entry. + */ + if (wsi->mux_substream && ah->unk_pos && ah->unk_value_pos) { + lws_ser_wu16be((uint8_t *)&ah->data[ah->unk_pos + + UHO_VLEN], + (uint16_t)(ah->pos - ah->unk_value_pos)); + ah->unk_pos = 0; + ah->unk_value_pos = 0; + } +#endif + h2n->is_first_header_char = 1; h2n->hpack = HPKS_TYPE; break; diff --git a/lib/roles/h2/ops-h2.c b/lib/roles/h2/ops-h2.c index 94242557d..111e48091 100644 --- a/lib/roles/h2/ops-h2.c +++ b/lib/roles/h2/ops-h2.c @@ -978,6 +978,21 @@ lws_h2_bind_for_post_before_action(struct lws *wsi) lws_buflist_use_segment(&wsi->buflist, blen); wsi->http.rx_content_length -= blen; + /* + * Keep rx_content_remain in step with the body we just + * delivered from the deferred buflist. The inline DATA path in + * lws_read_h1() and the HTTP_BODY_COMPLETION decision both track + * rx_content_remain (initialized to the full content-length); + * if we only decrement rx_content_length here, a body that is + * split across the DEFERRING_ACTION boundary (some stashed here, + * the rest arriving later inline) leaves rx_content_remain stuck + * at the stashed byte count, so completion never fires and the + * request times out. + */ + if (wsi->http.rx_content_remain >= blen) + wsi->http.rx_content_remain -= blen; + else + wsi->http.rx_content_remain = 0; } if (!wsi->buflist) diff --git a/lib/roles/http/parsers.c b/lib/roles/http/parsers.c index 18919a1c4..be0caf237 100644 --- a/lib/roles/http/parsers.c +++ b/lib/roles/http/parsers.c @@ -96,6 +96,7 @@ _lws_header_table_reset(struct allocated_headers *ah) ah->lextable_pos = 0; ah->unk_pos = 0; #if defined(LWS_WITH_CUSTOM_HEADERS) + ah->unk_value_pos = 0; ah->unk_ll_head = 0; ah->unk_ll_tail = 0; #endif @@ -612,7 +613,7 @@ lws_hdr_custom_length(struct lws *wsi, const char *name, int nlen) { ah_data_idx_t ll; - if (!wsi->http.ah || wsi->mux_substream) + if (!wsi->http.ah) return -1; ll = wsi->http.ah->unk_ll_head; @@ -638,7 +639,7 @@ lws_hdr_custom_copy(struct lws *wsi, char *dst, int len, const char *name, ah_data_idx_t ll; int n; - if (!wsi->http.ah || wsi->mux_substream) + if (!wsi->http.ah) return -1; *dst = '\0'; @@ -671,7 +672,7 @@ lws_hdr_custom_name_foreach(struct lws *wsi, lws_hdr_custom_fe_cb_t cb, { ah_data_idx_t ll; - if (!wsi->http.ah || wsi->mux_substream) + if (!wsi->http.ah) return -1; ll = wsi->http.ah->unk_ll_head; @@ -1221,10 +1222,18 @@ lws_parse(struct lws *wsi, unsigned char *buf, int *len) #endif } - if (lws_pos_in_bounds(wsi)) - return LPR_FAIL; + /* + * For mux (h2) substreams the hpack decoder captures + * the header name itself (including building the + * unknown-header storage), so we must not also lay the + * name bytes down here and double-advance ah->pos. + */ + if (!wsi->mux_substream) { + if (lws_pos_in_bounds(wsi)) + return LPR_FAIL; - ah->data[ah->pos++] = (char)c; + ah->data[ah->pos++] = (char)c; + } pos = ah->lextable_pos; #if defined(LWS_WITH_CUSTOM_HEADERS) diff --git a/lib/roles/http/server/server.c b/lib/roles/http/server/server.c index efa48d487..466283b06 100644 --- a/lib/roles/http/server/server.c +++ b/lib/roles/http/server/server.c @@ -1910,6 +1910,24 @@ lws_http_action(struct lws *wsi) lwsl_debug("%s: explicit 0 content-length\n", __func__); } } +#if defined(LWS_ROLE_H2) + else if (lwsi_role_h2(wsi) && wsi->mux_substream && wsi->h2.END_STREAM) { + /* + * h2 with no Content-Length, but END_STREAM already arrived on + * the HEADERS: the request body is empty and complete. Without + * this, a body-bearing method (POST/PUT/PATCH) would be left + * waiting on the 100MB default above for a body that will never + * come, stalling the request until it times out. Treat it as an + * explicit zero-length body, exactly as an explicit + * "Content-Length: 0" would (h3 does the equivalent in ops-h3.c). + */ + wsi->http.rx_content_remain = wsi->http.rx_content_length = 0; + wsi->http.content_length_given = 1; + wsi->http.content_length_explicitly_zero = 1; + lwsl_debug("%s: h2 END_STREAM, no content-length: empty body\n", + __func__); + } +#endif if (wsi->mux_substream) { wsi->http.request_version = HTTP_VERSION_2;