MEDIUM: mux-h2: support emitting CONTINUATION frames after HEADERS

There are some reports of users not being able to pass "enterprise"
traffic through haproxy when using H2 because it doesn't emit CONTINUATION
frames and as such is limited to headers no longer than the negociated
max-frame-size which usually is 16 kB.

This patch implements support form emitting CONTINUATION when a HEADERS
frame cannot fit within a limit of mfs. It does this by first filling a
buffer-wise frame, then truncating it starting from the tail to append
CONTINUATION frames. This makes sure that we can truncate on any byte
without being forced to stop on a header boundary, and ensures that the
common case (no fragmentation) doesn't add any extra cost. By moving
the tail first we make sure that each byte is moved only once, thus the
performance impact remains negligible.

This addresses github issue #249.
diff --git a/src/mux_h2.c b/src/mux_h2.c
index fc4b02f..00d257f 100644
--- a/src/mux_h2.c
+++ b/src/mux_h2.c
@@ -1170,6 +1170,54 @@
 	return ret;
 }
 
+
+/* try to fragment the headers frame present at the beginning of buffer <b>,
+ * enforcing a limit of <mfs> bytes per frame. Returns 0 on failure, 1 on
+ * success. Typical causes of failure include a buffer not large enough to
+ * add extra frame headers. The existing frame size is read in the current
+ * frame. Its EH flag will be cleared if CONTINUATION frames need to be added,
+ * and its length will be adjusted. The stream ID for continuation frames will
+ * be copied from the initial frame's.
+ */
+static int h2_fragment_headers(struct buffer *b, uint32_t mfs)
+{
+	size_t remain    = b->data - 9;
+	int extra_frames = (remain - 1) / mfs;
+	size_t fsize;
+	char *fptr;
+	int frame;
+
+	if (b->data <= mfs + 9)
+		return 1;
+
+	/* Too large a frame, we need to fragment it using CONTINUATION
+	 * frames. We start from the end and move tails as needed.
+	 */
+	if (b->data + extra_frames * 9 > b->size)
+		return 0;
+
+	for (frame = extra_frames; frame; frame--) {
+		fsize = ((remain - 1) % mfs) + 1;
+		remain -= fsize;
+
+		/* move data */
+		fptr = b->area + 9 + remain + (frame - 1) * 9;
+		memmove(fptr + 9, b->area + 9 + remain, fsize);
+		b->data += 9;
+
+		/* write new frame header */
+		h2_set_frame_size(fptr, fsize);
+		fptr[3] = H2_FT_CONTINUATION;
+		fptr[4] = (frame == extra_frames) ? H2_F_HEADERS_END_HEADERS : 0;
+		write_n32(fptr + 5, read_n32(b->area + 5));
+	}
+
+	b->area[4] &= ~H2_F_HEADERS_END_HEADERS;
+	h2_set_frame_size(b->area, remain);
+	return 1;
+}
+
+
 /* marks stream <h2s> as CLOSED and decrement the number of active streams for
  * its connection if the stream was not yet closed. Please use this exclusively
  * before closing a stream to ensure stream count is well maintained.
@@ -4623,6 +4671,18 @@
 		}
 	}
 
+	/* update the frame's size */
+	h2_set_frame_size(outbuf.area, outbuf.data - 9);
+
+	if (outbuf.data > h2c->mfs + 9) {
+		if (!h2_fragment_headers(&outbuf, h2c->mfs)) {
+			/* output full */
+			if (b_space_wraps(mbuf))
+				goto realign_again;
+			goto full;
+		}
+	}
+
 	/* we may need to add END_STREAM except for 1xx responses.
 	 * FIXME: we should also set it when we know for sure that the
 	 * content-length is zero as well as on 204/304
@@ -4634,9 +4694,6 @@
 	if (!h2s->cs || 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;
 
@@ -4912,6 +4969,18 @@
 		}
 	}
 
+	/* update the frame's size */
+	h2_set_frame_size(outbuf.area, outbuf.data - 9);
+
+	if (outbuf.data > h2c->mfs + 9) {
+		if (!h2_fragment_headers(&outbuf, h2c->mfs)) {
+			/* output full */
+			if (b_space_wraps(mbuf))
+				goto realign_again;
+			goto full;
+		}
+	}
+
 	/* we may need to add END_STREAM if we have no body :
 	 *  - request already closed, or :
 	 *  - no transfer-encoding, and :
@@ -4927,9 +4996,6 @@
 	if (!h2s->cs || 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;