lighttpd: patches from upstream 15505/head
authorGlenn Strauss <gstrauss@gluelogic.com>
Fri, 23 Apr 2021 23:06:27 +0000 (19:06 -0400)
committerGlenn Strauss <gstrauss@gluelogic.com>
Sat, 24 Apr 2021 02:12:59 +0000 (22:12 -0400)
- ignore Content-Length from backend if 101 Switching Protocols
- close HTTP/2 connection after bad password
- skip cert chain build for self-issued certs
- meson zstd fix
- ls-hpack upstream update
- discard some HTTP/2 DATA frames received after response

Signed-off-by: Glenn Strauss <gstrauss@gluelogic.com>
net/lighttpd/Makefile
net/lighttpd/files/lighttpd.conf
net/lighttpd/patches/030-101-upgrade-w-content-length.patch [new file with mode: 0644]
net/lighttpd/patches/040-mod_auth-close-http2-after-bad-pass.patch [new file with mode: 0644]
net/lighttpd/patches/050-openssl-skip-chain-build-self-issued.patch [new file with mode: 0644]
net/lighttpd/patches/060-meson-zstd.patch [new file with mode: 0644]
net/lighttpd/patches/070-ls-hpack-update.patch [new file with mode: 0644]
net/lighttpd/patches/080-http2-data-after-response.patch [new file with mode: 0644]

index 75947fb9500a728984c320fc9b72a87424bd042c..56e7b8161034cb538d6615f8983b0fd275eb3706 100644 (file)
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
 
 PKG_NAME:=lighttpd
 PKG_VERSION:=1.4.59
-PKG_RELEASE:=1
+PKG_RELEASE:=2
 # release candidate ~rcX testing; remove for release
 #PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-1.4.59
 
index 3846acf748aef5276d3ef77a92722df75f345b13..079d2a6884cf77b4795a6d9161c7807aa078aeda 100644 (file)
@@ -13,8 +13,6 @@ static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )
 
 ### Features
 #https://redmine.lighttpd.net/projects/lighttpd/wiki/Server_feature-flagsDetails
-server.feature-flags       += ("server.h2proto" => "enable")
-server.feature-flags       += ("server.h2c"     => "enable")
 server.feature-flags       += ("server.graceful-shutdown-timeout" => 5)
 #server.feature-flags       += ("server.graceful-restart-bg" => "enable")
 
diff --git a/net/lighttpd/patches/030-101-upgrade-w-content-length.patch b/net/lighttpd/patches/030-101-upgrade-w-content-length.patch
new file mode 100644 (file)
index 0000000..d461932
--- /dev/null
@@ -0,0 +1,31 @@
+From 1ca25d4e2cfeb83c844ad52b9c94eac218c71379 Mon Sep 17 00:00:00 2001
+From: Glenn Strauss <gstrauss@gluelogic.com>
+Date: Thu, 4 Feb 2021 00:22:12 -0500
+Subject: [PATCH] [core] 101 upgrade fails if Content-Length incl (fixes #3063)
+
+(thx daimh)
+
+commit 903024d7 in lighttpd 1.4.57 fixed issue #3046 but in the process
+broke HTTP/1.1 101 Switching Protocols which included Content-Length: 0
+in the response headers.  Content-Length response header is permitted
+by the RFCs, but not necessary with HTTP status 101 Switching Protocols.
+
+x-ref:
+  "websocket proxy fails if 101 Switching Protocols from backend includes Content-Length"
+  https://redmine.lighttpd.net/issues/3063
+
+Signed-off-by: Glenn Strauss <gstrauss@gluelogic.com>
+---
+ src/http-header-glue.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+--- a/src/http-header-glue.c
++++ b/src/http-header-glue.c
+@@ -961,6 +961,7 @@ void http_response_upgrade_read_body_unk
+           (FDEVENT_STREAM_RESPONSE_BUFMIN | FDEVENT_STREAM_RESPONSE);
+     r->conf.stream_request_body |= FDEVENT_STREAM_REQUEST_POLLIN;
+     r->reqbody_length = -2;
++    r->resp_body_scratchpad = -1;
+     r->keep_alive = 0;
+ }
diff --git a/net/lighttpd/patches/040-mod_auth-close-http2-after-bad-pass.patch b/net/lighttpd/patches/040-mod_auth-close-http2-after-bad-pass.patch
new file mode 100644 (file)
index 0000000..69c98f7
--- /dev/null
@@ -0,0 +1,143 @@
+From 4a600dabd5e2799bf0c3048859ee4f00808b7d89 Mon Sep 17 00:00:00 2001
+From: Glenn Strauss <gstrauss@gluelogic.com>
+Date: Sat, 6 Feb 2021 08:29:41 -0500
+Subject: [PATCH] [mod_auth] close HTTP/2 connection after bad pass
+
+mitigation slows down brute force password attacks
+
+x-ref:
+  "Possible feature: authentication brute force hardening"
+  https://redmine.lighttpd.net/boards/3/topics/8885
+
+Signed-off-by: Glenn Strauss <gstrauss@gluelogic.com>
+---
+ src/connections.c   | 22 +++++++++++++++++++++-
+ src/mod_accesslog.c |  2 +-
+ src/mod_auth.c      |  6 +++---
+ src/reqpool.c       |  1 +
+ src/request.h       |  2 +-
+ src/response.c      |  4 ++--
+ 6 files changed, 29 insertions(+), 8 deletions(-)
+
+--- a/src/connections.c
++++ b/src/connections.c
+@@ -228,7 +228,7 @@ static void connection_handle_response_e
+               }
+       }
+-        if (r->keep_alive) {
++        if (r->keep_alive > 0) {
+               request_reset(r);
+               config_reset_config(r);
+               con->is_readable = 1; /* potentially trigger optimistic read */
+@@ -1265,6 +1265,19 @@ connection_set_fdevent_interest (request
+ }
++__attribute_cold__
++static void
++connection_request_end_h2 (request_st * const h2r, connection * const con)
++{
++    if (h2r->keep_alive >= 0) {
++        h2r->keep_alive = -1;
++        h2_send_goaway(con, H2_E_NO_ERROR);
++    }
++    else /*(abort connection upon second request to close h2 connection)*/
++        h2_send_goaway(con, H2_E_ENHANCE_YOUR_CALM);
++}
++
++
+ static void
+ connection_state_machine_h2 (request_st * const h2r, connection * const con)
+ {
+@@ -1359,8 +1372,15 @@ connection_state_machine_h2 (request_st
+                     && !chunkqueue_is_empty(con->read_queue))
+                     resched |= 1;
+                 h2_send_end_stream(r, con);
++                const int alive = r->keep_alive;
+                 h2_retire_stream(r, con);/*r invalidated;removed from h2c->r[]*/
+                 --i;/* adjust loop i; h2c->rused was modified to retire r */
++                /*(special-case: allow *stream* to set r->keep_alive = -1 to
++                 * trigger goaway on h2 connection, e.g. after mod_auth failure
++                 * in attempt to mitigate brute force attacks by forcing a
++                 * reconnect and (somewhat) slowing down retries)*/
++                if (alive < 0)
++                    connection_request_end_h2(h2r, con);
+             }
+         }
+     }
+--- a/src/mod_accesslog.c
++++ b/src/mod_accesslog.c
+@@ -1108,7 +1108,7 @@ static int log_access_record (const requ
+                               break;
+                       case FORMAT_CONNECTION_STATUS:
+                               if (r->state == CON_STATE_RESPONSE_END) {
+-                                      if (0 == r->keep_alive) {
++                                      if (r->keep_alive <= 0) {
+                                               buffer_append_string_len(b, CONST_STR_LEN("-"));
+                                       } else {
+                                               buffer_append_string_len(b, CONST_STR_LEN("+"));
+--- a/src/mod_auth.c
++++ b/src/mod_auth.c
+@@ -828,7 +828,7 @@ static handler_t mod_auth_check_basic(re
+               log_error(r->conf.errh, __FILE__, __LINE__,
+                 "password doesn't match for %s username: %s IP: %s",
+                 r->uri.path.ptr, username->ptr, r->con->dst_addr_buf->ptr);
+-              r->keep_alive = 0; /*(disable keep-alive if bad password)*/
++              r->keep_alive = -1; /*(disable keep-alive if bad password)*/
+               rc = HANDLER_UNSET;
+               break;
+       }
+@@ -1461,7 +1461,7 @@ static handler_t mod_auth_check_digest(r
+               return HANDLER_FINISHED;
+       case HANDLER_ERROR:
+       default:
+-              r->keep_alive = 0; /*(disable keep-alive if unknown user)*/
++              r->keep_alive = -1; /*(disable keep-alive if unknown user)*/
+               buffer_free(b);
+               return mod_auth_send_401_unauthorized_digest(r, require, 0);
+       }
+@@ -1482,7 +1482,7 @@ static handler_t mod_auth_check_digest(r
+               log_error(r->conf.errh, __FILE__, __LINE__,
+                 "digest: auth failed for %s: wrong password, IP: %s",
+                 username, r->con->dst_addr_buf->ptr);
+-              r->keep_alive = 0; /*(disable keep-alive if bad password)*/
++              r->keep_alive = -1; /*(disable keep-alive if bad password)*/
+               buffer_free(b);
+               return mod_auth_send_401_unauthorized_digest(r, require, 0);
+--- a/src/reqpool.c
++++ b/src/reqpool.c
+@@ -58,6 +58,7 @@ request_reset (request_st * const r)
+     http_response_reset(r);
+     r->loops_per_request = 0;
++    r->keep_alive = 0;
+     r->h2state = 0; /* H2_STATE_IDLE */
+     r->h2id = 0;
+--- a/src/request.h
++++ b/src/request.h
+@@ -175,7 +175,7 @@ struct request_st {
+     char resp_header_repeated;
+     char loops_per_request;  /* catch endless loops in a single request */
+-    char keep_alive; /* only request.c can enable it, all other just disable */
++    int8_t keep_alive; /* only request.c can enable it, all other just disable */
+     char async_callback;
+     buffer *tmp_buf;                    /* shared; same as srv->tmp_buf */
+--- a/src/response.c
++++ b/src/response.c
+@@ -103,9 +103,9 @@ http_response_write_header (request_st *
+       if (light_btst(r->resp_htags, HTTP_HEADER_UPGRADE)
+           && r->http_version == HTTP_VERSION_1_1) {
+               http_header_response_set(r, HTTP_HEADER_CONNECTION, CONST_STR_LEN("Connection"), CONST_STR_LEN("upgrade"));
+-      } else if (0 == r->keep_alive) {
++      } else if (r->keep_alive <= 0) {
+               http_header_response_set(r, HTTP_HEADER_CONNECTION, CONST_STR_LEN("Connection"), CONST_STR_LEN("close"));
+-      } else if (r->http_version == HTTP_VERSION_1_0) {/*(&& r->keep_alive != 0)*/
++      } else if (r->http_version == HTTP_VERSION_1_0) {/*(&& r->keep_alive > 0)*/
+               http_header_response_set(r, HTTP_HEADER_CONNECTION, CONST_STR_LEN("Connection"), CONST_STR_LEN("keep-alive"));
+       }
diff --git a/net/lighttpd/patches/050-openssl-skip-chain-build-self-issued.patch b/net/lighttpd/patches/050-openssl-skip-chain-build-self-issued.patch
new file mode 100644 (file)
index 0000000..9577858
--- /dev/null
@@ -0,0 +1,45 @@
+From aa81834bc3ff47aa5cc66b6763678d3cf47a3d54 Mon Sep 17 00:00:00 2001
+From: Glenn Strauss <gstrauss@gluelogic.com>
+Date: Fri, 12 Mar 2021 20:03:38 -0500
+Subject: [PATCH] [mod_openssl] skip cert chain build if self-issued
+
+If cert is self-issued, then do not attempt to build certificate chain.
+
+(Attempting to build certificate chain when chain is not provided, but
+ ssl.ca-file is specified, is provided as backward compatible behavior
+ from lighttpd versions prior to lighttpd 1.4.56)
+
+Signed-off-by: Glenn Strauss <gstrauss@gluelogic.com>
+---
+ src/mod_openssl.c | 6 +++++-
+ 1 file changed, 5 insertions(+), 1 deletion(-)
+
+--- a/src/mod_openssl.c
++++ b/src/mod_openssl.c
+@@ -103,6 +103,7 @@ typedef struct {
+     time_t ssl_stapling_loadts;
+     time_t ssl_stapling_nextts;
+     char must_staple;
++    char self_issued;
+ } plugin_cert;
+ typedef struct {
+@@ -1081,7 +1082,7 @@ mod_openssl_cert_cb (SSL *ssl, void *arg
+    #if !defined(BORINGSSL_API_VERSION) \
+     && !defined(LIBRESSL_VERSION_NUMBER)
+     /* (missing SSL_set1_chain_cert_store() and SSL_build_cert_chain()) */
+-    else if (hctx->conf.ssl_ca_file) {
++    else if (hctx->conf.ssl_ca_file && !pc->self_issued) {
+         /* preserve legacy behavior whereby openssl will reuse CAs trusted for
+          * certificate verification (set by SSL_CTX_load_verify_locations() in
+          * SSL_CTX) in order to build certificate chain for server certificate
+@@ -1671,6 +1672,9 @@ network_openssl_load_pemfile (server *sr
+   #else
+     pc->must_staple = 0;
+   #endif
++    pc->self_issued =
++      (0 == X509_NAME_cmp(X509_get_subject_name(ssl_pemfile_x509),
++                          X509_get_issuer_name(ssl_pemfile_x509)));
+     if (!buffer_string_is_empty(pc->ssl_stapling_file)) {
+       #ifndef OPENSSL_NO_OCSP
diff --git a/net/lighttpd/patches/060-meson-zstd.patch b/net/lighttpd/patches/060-meson-zstd.patch
new file mode 100644 (file)
index 0000000..138b443
--- /dev/null
@@ -0,0 +1,27 @@
+From c41ebea4bb220c8fe252f472eec836c691734690 Mon Sep 17 00:00:00 2001
+From: Glenn Strauss <gstrauss@gluelogic.com>
+Date: Fri, 2 Apr 2021 01:01:02 -0400
+Subject: [PATCH] [build] fix zstd option in meson (fixes #3076)
+
+(thx KimonHoffmann)
+
+x-ref:
+  "Fix zstd dependency handling in meson build"
+  https://redmine.lighttpd.net/issues/3076
+
+Signed-off-by: Glenn Strauss <gstrauss@gluelogic.com>
+---
+ src/meson.build | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+--- a/src/meson.build
++++ b/src/meson.build
+@@ -685,7 +685,7 @@ endif
+ libzstd = []
+ if get_option('with_zstd')
+-      libz = dependency('zstd', required: false)
++      libzstd = dependency('zstd', required: false)
+       if libzstd.found()
+               libzstd = [ libzstd ]
+       else
diff --git a/net/lighttpd/patches/070-ls-hpack-update.patch b/net/lighttpd/patches/070-ls-hpack-update.patch
new file mode 100644 (file)
index 0000000..1267fa9
--- /dev/null
@@ -0,0 +1,56 @@
+From 3392e8fb11de35778cad1fb112e6eb5916aa7de0 Mon Sep 17 00:00:00 2001
+From: Glenn Strauss <gstrauss@gluelogic.com>
+Date: Tue, 20 Apr 2021 22:04:56 -0400
+Subject: [PATCH] [core] update ls-hpack
+
+LiteSpeed ls-hpack v2.3.0
+
+Signed-off-by: Glenn Strauss <gstrauss@gluelogic.com>
+---
+ src/ls-hpack/README.md | 2 +-
+ src/ls-hpack/lshpack.c | 4 +++-
+ src/ls-hpack/lshpack.h | 6 +++---
+ 3 files changed, 7 insertions(+), 5 deletions(-)
+
+--- a/src/ls-hpack/lshpack.c
++++ b/src/ls-hpack/lshpack.c
+@@ -1,7 +1,7 @@
+ /*
+ MIT License
+-Copyright (c) 2018 LiteSpeed Technologies Inc
++Copyright (c) 2018 - 2021 LiteSpeed Technologies Inc
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+@@ -1549,6 +1549,8 @@ lshpack_dec_push_entry (struct lshpack_d
+ #endif
+     memcpy(DTE_NAME(entry), lsxpack_header_get_name(xhdr), name_len);
+     memcpy(DTE_VALUE(entry), lsxpack_header_get_value(xhdr), val_len);
++
++    hdec_remove_overflow_entries(dec);
+     return 0;
+ }
+--- a/src/ls-hpack/lshpack.h
++++ b/src/ls-hpack/lshpack.h
+@@ -1,7 +1,7 @@
+ /*
+ MIT License
+-Copyright (c) 2018 - 2020 LiteSpeed Technologies Inc
++Copyright (c) 2018 - 2021 LiteSpeed Technologies Inc
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+@@ -34,8 +34,8 @@ extern "C" {
+ #include "lsxpack_header.h"
+ #define LSHPACK_MAJOR_VERSION 2
+-#define LSHPACK_MINOR_VERSION 2
+-#define LSHPACK_PATCH_VERSION 1
++#define LSHPACK_MINOR_VERSION 3
++#define LSHPACK_PATCH_VERSION 0
+ #define lshpack_strlen_t lsxpack_strlen_t
+ #define LSHPACK_MAX_STRLEN LSXPACK_MAX_STRLEN
diff --git a/net/lighttpd/patches/080-http2-data-after-response.patch b/net/lighttpd/patches/080-http2-data-after-response.patch
new file mode 100644 (file)
index 0000000..397aa27
--- /dev/null
@@ -0,0 +1,145 @@
+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);