MEDIUM: mux-h2: implement emission of H2 headers frames from HTX blocks

When using HTX, we need a separate function to emit a headers frame.
The code is significantly different from the H1 to H2 conversion, though
it borrows some parts there. It looks like the part building the H2 frame
from the headers list could be factored out, however some of the logic
around dealing with end of stream or block sizes remains different.

With this patch it becomes possible to retrieve bodyless HTTP responses
using H2 over HTX.
diff --git a/src/mux_h2.c b/src/mux_h2.c
index a19a319..f7b6517 100644
--- a/src/mux_h2.c
+++ b/src/mux_h2.c
@@ -3520,6 +3520,228 @@
 	return total;
 }
 
+/* Try to send a HEADERS frame matching HTX response present in HTX message
+ * <htx> for the H2 stream <h2s>. Returns the number of bytes sent. The caller
+ * must check the stream's status to detect any error which might have happened
+ * subsequently to a successful send. The htx blocks are automatically removed
+ * from the message. The htx message is assumed to be valid since produced from
+ * the internal code, hence it contains a start line, an optional series of
+ * header blocks and an end of header, otherwise an invalid frame could be
+ * emitted and the resulting htx message could be left in an inconsistent state.
+ */
+static size_t h2s_htx_frt_make_resp_headers(struct h2s *h2s, struct htx *htx)
+{
+	struct http_hdr list[MAX_HTTP_HDR];
+	struct h2c *h2c = h2s->h2c;
+	struct htx_blk *blk;
+	struct htx_blk *blk_end;
+	struct buffer outbuf;
+	struct htx_sl *sl;
+	enum htx_blk_type type;
+	int es_now = 0;
+	int ret = 0;
+	int hdr;
+	int idx;
+
+	if (h2c_mux_busy(h2c, h2s)) {
+		h2s->flags |= H2_SF_BLK_MBUSY;
+		return 0;
+	}
+
+	if (!h2_get_buf(h2c, &h2c->mbuf)) {
+		h2c->flags |= H2_CF_MUX_MALLOC;
+		h2s->flags |= H2_SF_BLK_MROOM;
+		return 0;
+	}
+
+	/* determine the first block which must not be deleted, blk_end may
+	 * be NULL if all blocks have to be deleted.
+	 */
+	idx = htx_get_head(htx);
+	blk_end = NULL;
+	while (idx != -1) {
+		type = htx_get_blk_type(htx_get_blk(htx, idx));
+		idx = htx_get_next(htx, idx);
+		if (type == HTX_BLK_EOH) {
+			if (idx != -1)
+				blk_end = htx_get_blk(htx, idx);
+			break;
+		}
+	}
+
+	/* get the start line, we do have one */
+	blk = htx_get_blk(htx, htx->sl_off);
+	sl = htx_get_blk_ptr(htx, blk);
+	h2s->status = sl->info.res.status;
+
+	/* and the rest of the headers, that we dump starting at header 0 */
+	hdr = 0;
+
+	idx = htx_get_next(htx, htx->sl_off);
+	while ((idx = htx_get_next(htx, idx)) != -1) {
+		blk = htx_get_blk(htx, idx);
+		type = htx_get_blk_type(blk);
+
+		if (type == HTX_BLK_UNUSED)
+			continue;
+
+		if (type != HTX_BLK_HDR)
+			break;
+
+		if (unlikely(hdr >= sizeof(list)/sizeof(list[0]) - 1))
+			goto fail;
+
+		list[hdr].n = htx_get_blk_name(htx, blk);
+		list[hdr].v = htx_get_blk_value(htx, blk);
+
+#if 1
+		{
+			/* FIXME: header names MUST be lower case in H2. For now it's
+			 * not granted by HTX so let's force them now.
+			 */
+			char *p;
+			for (p = list[hdr].n.ptr; p != list[hdr].n.ptr + list[hdr].n.len; p++)
+				if (unlikely(isupper(*p)))
+					*p = tolower(*p);
+		}
+#endif
+		hdr++;
+	}
+
+	/* marker for end of headers */
+	list[hdr].n = ist("");
+
+	if (h2s->status == 204 || h2s->status == 304) {
+		/* no contents, claim c-len is present and set to zero */
+		es_now = 1;
+	}
+
+	chunk_reset(&outbuf);
+
+	while (1) {
+		outbuf.area  = b_tail(&h2c->mbuf);
+		outbuf.size = b_contig_space(&h2c->mbuf);
+		outbuf.data = 0;
+
+		if (outbuf.size >= 9 || !b_space_wraps(&h2c->mbuf))
+			break;
+	realign_again:
+		b_slow_realign(&h2c->mbuf, trash.area, b_data(&h2c->mbuf));
+	}
+
+	if (outbuf.size < 9)
+		goto full;
+
+	/* len: 0x000000 (fill later), type: 1(HEADERS), flags: ENDH=4 */
+	memcpy(outbuf.area, "\x00\x00\x00\x01\x04", 5);
+	write_n32(outbuf.area + 5, h2s->id); // 4 bytes
+	outbuf.data = 9;
+
+	/* encode status, which necessarily is the first one */
+	if (outbuf.data < outbuf.size && h2s->status == 200)
+		outbuf.area[outbuf.data++] = 0x88; // indexed field : idx[08]=(":status", "200")
+	else if (outbuf.data < outbuf.size && h2s->status == 304)
+		outbuf.area[outbuf.data++] = 0x8b; // indexed field : idx[11]=(":status", "304")
+	else if (unlikely(h2s->status < 100 || h2s->status > 999)) {
+		/* this is an unparsable response */
+		goto fail;
+	}
+	else if (unlikely(outbuf.data + 2 + 3 <= outbuf.size)) {
+		/* basic encoding of the status code */
+		outbuf.area[outbuf.data++] = 0x48; // indexed name -- name=":status" (idx 8)
+		outbuf.area[outbuf.data++] = 0x03; // 3 bytes status
+		outbuf.area[outbuf.data++] = '0' + h2s->status / 100;
+		outbuf.area[outbuf.data++] = '0' + h2s->status / 10 % 10;
+		outbuf.area[outbuf.data++] = '0' + h2s->status % 10;
+	}
+	else {
+		if (b_space_wraps(&h2c->mbuf))
+			goto realign_again;
+		goto full;
+	}
+
+	/* encode all headers, stop at empty name */
+	for (hdr = 0; hdr < sizeof(list)/sizeof(list[0]); hdr++) {
+		/* these ones do not exist in H2 and must be dropped. */
+		if (isteq(list[hdr].n, ist("connection")) ||
+		    isteq(list[hdr].n, ist("proxy-connection")) ||
+		    isteq(list[hdr].n, ist("keep-alive")) ||
+		    isteq(list[hdr].n, ist("upgrade")) ||
+		    isteq(list[hdr].n, ist("transfer-encoding")))
+			continue;
+
+		if (isteq(list[hdr].n, ist("")))
+			break; // end
+
+		if (!hpack_encode_header(&outbuf, list[hdr].n, list[hdr].v)) {
+			/* output full */
+			if (b_space_wraps(&h2c->mbuf))
+				goto realign_again;
+			goto full;
+		}
+	}
+
+	/* we may need to add END_STREAM.
+	 * FIXME: we should also set it when we know for sure that the
+	 * content-length is zero as well as on 204/304
+	 */
+	if (blk_end && htx_get_blk_type(blk_end) == HTX_BLK_EOM)
+		es_now = 1;
+
+	if (h2s->cs->flags & CS_FL_SHW)
+		es_now = 1;
+
+	/* update the frame's size */
+	h2_set_frame_size(outbuf.area, outbuf.data - 9);
+
+	if (es_now)
+		outbuf.area[4] |= H2_F_HEADERS_END_STREAM;
+
+	/* commit the H2 response */
+	b_add(&h2c->mbuf, outbuf.data);
+	h2s->flags |= H2_SF_HEADERS_SENT;
+
+	/* for now we don't implemented CONTINUATION, so we wait for a
+	 * body or directly end in TRL2.
+	 */
+	if (es_now) {
+		h2s->flags |= H2_SF_ES_SENT;
+		if (h2s->st == H2_SS_OPEN)
+			h2s->st = H2_SS_HLOC;
+		else
+			h2s_close(h2s);
+	}
+
+	/* OK we could properly deliver the response */
+
+	/* remove all header blocks including the EOH and compute the
+	 * corresponding size.
+	 *
+	 * FIXME: We should remove everything when es_now is set.
+	 */
+	ret = 0;
+	idx = htx_get_head(htx);
+	blk = htx_get_blk(htx, idx);
+	while (blk != blk_end) {
+		ret += htx_get_blksz(blk);
+		blk = htx_remove_blk(htx, blk);
+	}
+ end:
+	return ret;
+ full:
+	h2c->flags |= H2_CF_MUX_MFULL;
+	h2s->flags |= H2_SF_BLK_MROOM;
+	ret = 0;
+	goto end;
+ fail:
+	/* unparsable HTX messages, too large ones to be produced in the local
+	 * list etc go here (unrecoverable errors).
+	 */
+	h2s_error(h2s, H2_ERR_INTERNAL_ERROR);
+	ret = 0;
+	goto end;
+}
+
 /* Called from the upper layer, to subscribe to events, such as being able to send */
 static int h2_subscribe(struct conn_stream *cs, int event_type, void *param)
 {
@@ -3697,6 +3919,17 @@
 			bsize = htx_get_blksz(blk);
 
 			switch (btype) {
+			case HTX_BLK_RES_SL:
+				/* start-line before headers */
+				ret = h2s_htx_frt_make_resp_headers(h2s, htx);
+				if (ret > 0) {
+					total += ret;
+					count -= ret;
+					if (ret < bsize)
+						goto done;
+				}
+				break;
+
 			default:
 				htx_remove_blk(htx, blk);
 				total += bsize;