From 81d18a8e359685c169cfd30e6a1574b98aedbaeb Mon Sep 17 00:00:00 2001
From: Glenn Strauss <gstrauss@gluelogic.com>
Date: Thu, 22 Apr 2021 01:11:47 -0400
Subject: [PATCH] [core] discard some HTTP/2 DATA after response (fixes #3078)

(thx oldium)
    
improve handling of HTTP/2 DATA frames received
a short time after sending response

x-ref:
  "POST request DATA part for non-existing URI closes HTTP/2 connection prematurely"
  https://redmine.lighttpd.net/issues/3078

Signed-off-by: Glenn Strauss <gstrauss@gluelogic.com>
---
 src/h2.c | 64 ++++++++++++++++++++++++++++++++++++++++++--------------
 src/h2.h |  1 +
 2 files changed, 49 insertions(+), 16 deletions(-)

--- a/src/h2.c
+++ b/src/h2.c
@@ -272,10 +272,23 @@ h2_send_rst_stream_id (uint32_t h2id, co
 
 __attribute_cold__
 static void
-h2_send_rst_stream (request_st * const r, connection * const con, const request_h2error_t e)
+h2_send_rst_stream_state (request_st * const r, h2con * const h2c)
 {
+    if (r->h2state != H2_STATE_HALF_CLOSED_REMOTE
+        && r->h2state != H2_STATE_CLOSED) {
+        /* set timestamp for comparison; not tracking individual stream ids */
+        h2c->half_closed_ts = log_epoch_secs;
+    }
     r->state = CON_STATE_ERROR;
     r->h2state = H2_STATE_CLOSED;
+}
+
+
+__attribute_cold__
+static void
+h2_send_rst_stream (request_st * const r, connection * const con, const request_h2error_t e)
+{
+    h2_send_rst_stream_state(r, con->h2);/*(sets r->h2state = H2_STATE_CLOSED)*/
     h2_send_rst_stream_id(r->h2id, con, e);
 }
 
@@ -289,13 +302,10 @@ h2_send_goaway_rst_stream (connection *
     for (uint32_t i = 0, rused = h2c->rused; i < rused; ++i) {
         request_st * const r = h2c->r[i];
         if (r->h2state == H2_STATE_CLOSED) continue;
+        h2_send_rst_stream_state(r, h2c);/*(sets r->h2state = H2_STATE_CLOSED)*/
         /*(XXX: might consider always sending RST_STREAM)*/
-        if (!sent_goaway) {
-            r->state = CON_STATE_ERROR;
-            r->h2state = H2_STATE_CLOSED;
-        }
-        else /*(also sets r->h2state = H2_STATE_CLOSED)*/
-            h2_send_rst_stream(r, con, H2_E_PROTOCOL_ERROR);
+        if (sent_goaway)
+            h2_send_rst_stream_id(r->h2id, con, H2_E_PROTOCOL_ERROR);
     }
 }
 
@@ -780,14 +790,27 @@ h2_recv_data (connection * const con, co
     }
     chunkqueue * const cq = con->read_queue;
     if (NULL == r) {
-        /* XXX: TODO: might need to keep a list of recently retired streams
-         * for a few seconds so that if we send RST_STREAM, then we ignore
-         * further DATA and do not send connection error, though recv windows
-         * still must be updated. */
-        if (h2c->h2_cid < id || (!h2c->sent_goaway && 0 != alen))
-            h2_send_goaway_e(con, H2_E_PROTOCOL_ERROR);
+        /* simplistic heuristic to discard additional DATA from recently-closed
+         * streams (or half-closed (local)), where recently-closed here is
+         * within 2-3 seconds of any (other) stream being half-closed (local)
+         * or reset before that (other) stream received END_STREAM from peer.
+         * (e.g. clients might fire off POST request followed by DATA,
+         *  and a response might be sent before processing DATA frames)
+         * (id <= h2c->h2_cid) already checked above, else H2_E_PROTOCOL_ERROR
+         * If the above conditions do not hold, then send GOAWAY to attempt to
+         * reduce the chance of becoming an infinite data sink for misbehaving
+         * clients, though remaining streams are still handled before the
+         * connection is closed. */
         chunkqueue_mark_written(cq, 9+len);
-        return 0;
+        if (h2c->half_closed_ts + 2 >= log_epoch_secs) {
+            h2_send_window_update(con, 0, len); /*(h2r->h2_rwin)*/
+            return 1;
+        }
+        else {
+            if (!h2c->sent_goaway && 0 != alen)
+                h2_send_goaway_e(con, H2_E_NO_ERROR);
+            return 0;
+        }
     }
 
     if (r->h2state == H2_STATE_CLOSED
@@ -808,7 +831,7 @@ h2_recv_data (connection * const con, co
         }
     }
     /*(allow h2r->h2_rwin to dip below 0 so that entire frame is processed)*/
-    /*(undeflow will not occur (with reasonable SETTINGS_MAX_FRAME_SIZE used)
+    /*(underflow will not occur (with reasonable SETTINGS_MAX_FRAME_SIZE used)
      * since windows updated elsewhere and data is streamed to temp files if
      * not FDEVENT_STREAM_REQUEST_BUFMIN)*/
     /*r->h2_rwin -= (int32_t)len;*/
@@ -2347,16 +2370,25 @@ h2_send_end_stream_data (request_st * co
     } };
 
     dataframe.u[2] = htonl(r->h2id);
-    r->h2state = H2_STATE_CLOSED;
     /*(ignore window updates when sending 0-length DATA frame with END_STREAM)*/
     chunkqueue_append_mem(con->write_queue,  /*(+3 to skip over align pad)*/
                           (const char *)dataframe.c+3, sizeof(dataframe)-3);
+
+    if (r->h2state != H2_STATE_HALF_CLOSED_REMOTE) {
+        /* set timestamp for comparison; not tracking individual stream ids */
+        h2con * const h2c = con->h2;
+        h2c->half_closed_ts = log_epoch_secs;
+        /* indicate to peer that no more DATA should be sent from peer */
+        h2_send_rst_stream_id(r->h2id, con, H2_E_NO_ERROR);
+    }
+    r->h2state = H2_STATE_CLOSED;
 }
 
 
 void
 h2_send_end_stream (request_st * const r, connection * const con)
 {
+    if (r->h2state == H2_STATE_CLOSED) return;
     if (r->state != CON_STATE_ERROR && r->resp_body_finished) {
         /* CON_STATE_RESPONSE_END */
         if (r->gw_dechunk && r->gw_dechunk->done
--- a/src/h2.h
+++ b/src/h2.h
@@ -92,6 +92,7 @@ struct h2con {
     uint32_t s_max_header_list_size;   /* SETTINGS_MAX_HEADER_LIST_SIZE   */
     struct lshpack_dec decoder;
     struct lshpack_enc encoder;
+      time_t half_closed_ts;
 };
 
 void h2_send_goaway (connection *con, request_h2error_t e);