h2: unify immortal stream tracking across SSE and ws substreams

It was already correct but add helpers to isolate and deduplicate
processing adding and closing a generically immortal stream.

Change the default 31s h2 network connection timeout to be settable
by .keepalive_timeout if nonzero.

Add a public api allowing a client h2 stream to transition to
half-closed LOCAL (by sending a 0-byte DATA with END_STREAM) and
mark itself as immortal to create a read-only long-poll stream
if the server allows it.

Add a vhost server option flag LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL
which allows the vhost to treat half-closed remotes as immortal long
poll streams.
diff --git a/READMEs/README.h2-long-poll.md b/READMEs/README.h2-long-poll.md
new file mode 100644
index 0000000..ab8c25f
--- /dev/null
+++ b/READMEs/README.h2-long-poll.md
@@ -0,0 +1,55 @@
+# h2 long poll in lws
+
+lws server and client can support "immortal" streams that are
+not subject to normal timeouts under a special condition.  These
+are read-only (to the client).
+
+Network connections that contain at least one immortal stream
+are themselves not subject to timeouts until the last immortal
+stream they are carrying closes.
+
+Because of this, it's recommended there is some other way of
+confirming that the client is still active.
+
+## Setting up lws server for h2 long poll
+
+Vhosts that wish to allow clients to serve these immortal
+streams need to set the info.options flag `LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL`
+at vhost creation time.  The JSON config equivalent is to set
+
+```
+"h2-half-closed-long-poll": "1"
+```
+
+on the vhost.  That's all that is needed.
+
+Streams continue to act normally for timeout with the exception
+client streams are allowed to signal they are half-closing by
+sending a zero-length DATA frame with END_STREAM set.  These
+streams are allowed to exist outside of any timeout and data
+can be sent on them at will in the server -> client direction.
+
+## Setting client streams for long poll
+
+An API is provided to allow established h2 client streams to
+transition to immortal mode and send the END_STREAM to the server
+to indicate it.
+
+```
+int
+lws_h2_client_stream_long_poll_rxonly(struct lws *wsi);
+```
+
+## Example applications
+
+You can confirm the long poll flow simply using example applications.
+Build and run `http-server/minimal-http-server-h2-long-poll` in one
+terminal.
+
+In another, build the usual `http-client/minimal-http-client` example
+and run it with the flags `-l --long-poll`
+
+The client will connect to the server and transition to the immortal mode.
+The server sends a timestamp every minute to the client, and that will
+stay up without timeouts.
+
diff --git a/include/libwebsockets/lws-context-vhost.h b/include/libwebsockets/lws-context-vhost.h
index bc9feff..6f741d6 100644
--- a/include/libwebsockets/lws-context-vhost.h
+++ b/include/libwebsockets/lws-context-vhost.h
@@ -212,6 +212,12 @@
 	/**< (VH) Indicates the connections using this vhost should ignore
 	 * h2 WINDOW_UPDATE from broken peers and fix them up */
 
+#define LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL		 (1ll << 32)
+	/**< (VH) Tell the vhost to treat half-closed remote clients as
+	 * entered into an immortal (ie, not subject to normal timeouts) long
+	 * poll mode.
+	 */
+
 	/****** add new things just above ---^ ******/
 
 
@@ -398,8 +404,9 @@
 	/**< VHOST: pointer to optional linked list of per-vhost
 	 * options made accessible to protocols */
 	int keepalive_timeout;
-	/**< VHOST: (default = 0 = 5s) seconds to allow remote
-	 * client to hold on to an idle HTTP/1.1 connection */
+	/**< VHOST: (default = 0 = 5s, 31s for http/2) seconds to allow remote
+	 * client to hold on to an idle HTTP/1.1 connection.  Timeout lifetime
+	 * applied to idle h2 network connections */
 	const char *log_filepath;
 	/**< VHOST: filepath to append logs to... this is opened before
 	 *		any dropping of initial privileges */
diff --git a/include/libwebsockets/lws-http.h b/include/libwebsockets/lws-http.h
index 863655e..fce21d8 100644
--- a/include/libwebsockets/lws-http.h
+++ b/include/libwebsockets/lws-http.h
@@ -746,6 +746,24 @@
 lws_http_mark_sse(struct lws *wsi);
 
 /**
+ * lws_h2_client_stream_long_poll_rxonly() - h2 stream to immortal read-only
+ *
+ * \param wsi: h2 stream client wsi
+ *
+ * Send END_STREAM-flagged zero-length DATA frame to set client stream wsi into
+ * half-closed (local) and remote into half-closed (remote).  Set the client
+ * stream wsi to be immortal (not subject to timeouts).
+ *
+ * Used if the remote server supports immortal long poll to put the stream into
+ * a read-only state where it can wait as long as needed for rx.
+ *
+ * Returns 0 if the process (which happens asynchronously) started or non-zero
+ * if it wasn't an h2 stream.
+ */
+LWS_VISIBLE LWS_EXTERN int
+lws_h2_client_stream_long_poll_rxonly(struct lws *wsi);
+
+/**
  * lws_http_compression_apply() - apply an http compression transform
  *
  * \param wsi: the wsi to apply the compression transform to
diff --git a/lib/core-net/close.c b/lib/core-net/close.c
index 7803ccc..aeac2e4 100644
--- a/lib/core-net/close.c
+++ b/lib/core-net/close.c
@@ -254,6 +254,11 @@
 	lws_addrinfo_clean(wsi);
 #endif
 
+#if defined(LWS_WITH_HTTP2)
+	if (wsi->h2_stream_immortal)
+		lws_http_close_immortal(wsi);
+#endif
+
 	/* if we have children, close them first */
 	if (wsi->child_list) {
 		wsi2 = wsi->child_list;
diff --git a/lib/core-net/private-lib-core-net.h b/lib/core-net/private-lib-core-net.h
index d6bc0a7..bcb73f0 100644
--- a/lib/core-net/private-lib-core-net.h
+++ b/lib/core-net/private-lib-core-net.h
@@ -676,8 +676,9 @@
 	unsigned int hdr_parsing_completed:1;
 	unsigned int http2_substream:1;
 	unsigned int upgraded_to_http2:1;
-	unsigned int h2_stream_carries_ws:1;
-	unsigned int h2_stream_carries_sse:1;
+	unsigned int h2_stream_immortal:1;
+	unsigned int h2_stream_carries_ws:1; /* immortal set as well */
+	unsigned int h2_stream_carries_sse:1; /* immortal set as well */
 	unsigned int seen_nonpseudoheader:1;
 	unsigned int listener:1;
 	unsigned int user_space_externally_allocated:1;
@@ -1086,20 +1087,25 @@
 #define lws_access_log(_a)
 #endif
 
-LWS_EXTERN int
+void
+lws_http_mark_immortal(struct lws *wsi);
+void
+lws_http_close_immortal(struct lws *wsi);
+
+int
 lws_cgi_kill_terminated(struct lws_context_per_thread *pt);
 
-LWS_EXTERN void
+void
 lws_cgi_remove_and_kill(struct lws *wsi);
 
-LWS_EXTERN void
+void
 lws_plat_delete_socket_from_fds(struct lws_context *context,
 				struct lws *wsi, int m);
-LWS_EXTERN void
+void
 lws_plat_insert_socket_into_fds(struct lws_context *context,
 				struct lws *wsi);
 
-LWS_EXTERN int
+int
 lws_plat_change_pollfd(struct lws_context *context, struct lws *wsi,
 		       struct lws_pollfd *pfd);
 
diff --git a/lib/core-net/wsi-timeout.c b/lib/core-net/wsi-timeout.c
index 1609142..db026c4 100644
--- a/lib/core-net/wsi-timeout.c
+++ b/lib/core-net/wsi-timeout.c
@@ -158,6 +158,8 @@
 	if (secs == LWS_TO_KILL_ASYNC)
 		secs = 0;
 
+	assert(!wsi->h2_stream_immortal);
+
 	lws_pt_lock(pt, __func__);
 	__lws_set_timeout(wsi, reason, secs);
 	lws_pt_unlock(pt);
diff --git a/lib/core-net/wsi.c b/lib/core-net/wsi.c
index 11c14f9..3051c48 100644
--- a/lib/core-net/wsi.c
+++ b/lib/core-net/wsi.c
@@ -830,20 +830,69 @@
 	return 0;
 }
 
+void
+lws_http_close_immortal(struct lws *wsi)
+{
+	struct lws *nwsi;
+
+	if (!wsi->http2_substream)
+		return;
+
+	assert(wsi->h2_stream_immortal);
+	wsi->h2_stream_immortal = 0;
+
+	nwsi = lws_get_network_wsi(wsi);
+	lwsl_debug("%s: %p %p %d\n", __func__, wsi, nwsi,
+				     nwsi->immortal_substream_count);
+	assert(nwsi->immortal_substream_count);
+	nwsi->immortal_substream_count--;
+	if (!nwsi->immortal_substream_count)
+		/*
+		 * since we closed the only immortal stream on this nwsi, we
+		 * need to reapply a normal timeout regime to the nwsi
+		 */
+		lws_set_timeout(nwsi, PENDING_TIMEOUT_HTTP_KEEPALIVE_IDLE,
+				wsi->vhost->keepalive_timeout ?
+				    wsi->vhost->keepalive_timeout : 31);
+}
+
+void
+lws_http_mark_immortal(struct lws *wsi)
+{
+	struct lws *nwsi;
+
+	lws_set_timeout(wsi, NO_PENDING_TIMEOUT, 0);
+
+	if (!wsi->http2_substream
+#if defined(LWS_WITH_CLIENT)
+			&& !wsi->client_h2_substream
+#endif
+	) {
+		lwsl_err("%s: not h2 substream\n", __func__);
+		return;
+	}
+
+	nwsi = lws_get_network_wsi(wsi);
+
+	lwsl_debug("%s: %p %p %d\n", __func__, wsi, nwsi,
+				     nwsi->immortal_substream_count);
+
+	wsi->h2_stream_immortal = 1;
+	assert(nwsi->immortal_substream_count < 255); /* largest count */
+	nwsi->immortal_substream_count++;
+	if (nwsi->immortal_substream_count == 1)
+		lws_set_timeout(nwsi, NO_PENDING_TIMEOUT, 0);
+}
+
+
 int
 lws_http_mark_sse(struct lws *wsi)
 {
 	lws_http_headers_detach(wsi);
-	lws_set_timeout(wsi, NO_PENDING_TIMEOUT, 0);
+	lws_http_mark_immortal(wsi);
 
-	if (wsi->http2_substream) {
-		struct lws *nwsi = lws_get_network_wsi(wsi);
-
+	if (wsi->http2_substream)
 		wsi->h2_stream_carries_sse = 1;
-		nwsi->immortal_substream_count++;
-		if (nwsi->immortal_substream_count == 1)
-			lws_set_timeout(nwsi, NO_PENDING_TIMEOUT, 0);
-	}
 
 	return 0;
 }
diff --git a/lib/roles/h2/http2.c b/lib/roles/h2/http2.c
index 331646c..29aa9e7 100644
--- a/lib/roles/h2/http2.c
+++ b/lib/roles/h2/http2.c
@@ -845,11 +845,9 @@
 	/* let the network wsi live a bit longer if subs are active */
 
 	if (!wsi->immortal_substream_count)
-#if defined(LWS_AMAZON_RTOS) || defined(LWS_AMAZON_LINUX)
-		lws_set_timeout(wsi, PENDING_TIMEOUT_HTTP_KEEPALIVE_IDLE, wsi->vhost->keepalive_timeout);
-#else
-		lws_set_timeout(wsi, PENDING_TIMEOUT_HTTP_KEEPALIVE_IDLE, 31);
-#endif
+		lws_set_timeout(wsi, PENDING_TIMEOUT_HTTP_KEEPALIVE_IDLE,
+				wsi->vhost->keepalive_timeout ?
+					wsi->vhost->keepalive_timeout : 31);
 
 	if (h2n->sid)
 		h2n->swsi = lws_h2_wsi_from_id(wsi, h2n->sid);
@@ -1164,12 +1162,24 @@
 			assert(w->h2.sibling_list != w);
 		} lws_end_foreach_ll(w, h2.sibling_list);
 
+		if (lws_check_opt(h2n->swsi->vhost->options,
+			       LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL)) {
 
-		/* END_STREAM means after servicing this, close the stream */
-		h2n->swsi->h2.END_STREAM =
+			/*
+			 * We don't directly timeout streams that enter the
+			 * half-closed remote state, allowing immortal long
+			 * poll
+			 */
+			lws_http_mark_immortal(h2n->swsi);
+			lwsl_info("%s: %p: h2 stream entering long poll\n",
+					__func__, h2n->swsi);
+
+		} else {
+			h2n->swsi->h2.END_STREAM =
 					!!(h2n->flags & LWS_H2_FLAG_END_STREAM);
-		lwsl_info("%s: hdr END_STREAM = %d\n",__func__,
+			lwsl_debug("%s: hdr END_STREAM = %d\n",__func__,
 			  h2n->swsi->h2.END_STREAM);
+		}
 
 		h2n->cont_exp = !(h2n->flags & LWS_H2_FLAG_END_HEADERS);
 		h2n->cont_exp_sid = h2n->sid;
@@ -1863,12 +1873,9 @@
 				 */
 				if (!wsi->immortal_substream_count)
 					lws_set_timeout(wsi,
-					  PENDING_TIMEOUT_HTTP_KEEPALIVE_IDLE,
-#if defined(LWS_AMAZON_RTOS) || defined(LWS_AMAZON_LINUX)
-					  wsi->vhost->keepalive_timeout);
-#else
-					  31);
-#endif
+					PENDING_TIMEOUT_HTTP_KEEPALIVE_IDLE,
+						wsi->vhost->keepalive_timeout ?
+					    wsi->vhost->keepalive_timeout : 31);
 
 				if (!h2n->swsi)
 					break;
@@ -2411,3 +2418,21 @@
 	return lws_ptr_diff(buf, oldbuf);
 }
 
+int
+lws_h2_client_stream_long_poll_rxonly(struct lws *wsi)
+{
+
+	if (!wsi->http2_substream)
+		return 1;
+
+	/*
+	 * Elect to send an empty DATA with END_STREAM, to force the stream
+	 * into HALF_CLOSED LOCAL
+	 */
+	wsi->h2.long_poll = 1;
+	wsi->h2.send_END_STREAM = 1;
+
+	lws_callback_on_writable(wsi);
+
+	return 0;
+}
diff --git a/lib/roles/h2/ops-h2.c b/lib/roles/h2/ops-h2.c
index dd57ce4..de65739 100644
--- a/lib/roles/h2/ops-h2.c
+++ b/lib/roles/h2/ops-h2.c
@@ -386,20 +386,22 @@
 
 	/* if not in a state to send stuff, then just send nothing */
 
-	if (!lwsi_role_ws(wsi) &&
+	if (!lwsi_role_ws(wsi) && !wsi->h2_stream_immortal &&
 	    base != LWS_WRITE_HTTP &&
 	    base != LWS_WRITE_HTTP_FINAL &&
 	    base != LWS_WRITE_HTTP_HEADERS_CONTINUATION &&
 	    base != LWS_WRITE_HTTP_HEADERS &&
 	    ((lwsi_state(wsi) != LRS_RETURNED_CLOSE &&
 	      lwsi_state(wsi) != LRS_WAITING_TO_SEND_CLOSE &&
+	      lwsi_state(wsi) != LRS_ESTABLISHED &&
 	      lwsi_state(wsi) != LRS_AWAITING_CLOSE_ACK)
 #if defined(LWS_ROLE_WS)
 	   || base != LWS_WRITE_CLOSE
 #endif
 	)) {
 		//assert(0);
-		lwsl_notice("binning wsistate 0x%x %d\n", wsi->wsistate, *wp);
+		lwsl_notice("%s: binning wsistate 0x%x %d\n", __func__,
+				wsi->wsistate, *wp);
 		return 0;
 	}
 
@@ -486,7 +488,6 @@
 rops_check_upgrades_h2(struct lws *wsi)
 {
 #if defined(LWS_ROLE_WS)
-	struct lws *nwsi;
 	char *p;
 
 	/*
@@ -504,21 +505,16 @@
 	if (!p || strcmp(p, "websocket"))
 		return LWS_UPG_RET_CONTINUE;
 
-	nwsi = lws_get_network_wsi(wsi);
-
 #if defined(LWS_WITH_SERVER_STATUS)
 	wsi->vhost->conn_stats.ws_upg++;
 #endif
 	lwsl_info("Upgrade h2 to ws\n");
+	lws_http_mark_immortal(wsi);
 	wsi->h2_stream_carries_ws = 1;
-	nwsi->immortal_substream_count++;
+
 	if (lws_process_ws_upgrade(wsi))
 		return LWS_UPG_RET_BAIL;
 
-	if (nwsi->immortal_substream_count == 1)
-		lws_set_timeout(nwsi, NO_PENDING_TIMEOUT, 0);
-
-	lws_set_timeout(wsi, NO_PENDING_TIMEOUT, 0);
 	lwsl_info("Upgraded h2 to ws OK\n");
 
 	return LWS_UPG_RET_DONE;
@@ -717,16 +713,6 @@
 			lws_free_set_NULL(wsi->h2.pending_status_body);
 	}
 
-	if (wsi->h2_stream_carries_ws || wsi->h2_stream_carries_sse) {
-		struct lws *nwsi = lws_get_network_wsi(wsi);
-
-		nwsi->immortal_substream_count--;
-		/* if no ws, then put a timeout on the parent wsi */
-		if (!nwsi->immortal_substream_count)
-			__lws_set_timeout(nwsi,
-				PENDING_TIMEOUT_HTTP_KEEPALIVE_IDLE, 31);
-	}
-
 	return 0;
 }
 
@@ -1148,6 +1134,30 @@
 			goto next_child;
 		}
 #endif
+
+		/*
+		 * set client wsi to immortal long-poll mode; send END_STREAM
+		 * flag on headers to indicate to a server, that allows
+		 * it, that you want them to leave the stream in a long poll
+		 * ro immortal state.  We have to send headers so the client
+		 * understands the http connection is ongoing.
+		 */
+
+		if (w->h2.send_END_STREAM && w->h2.long_poll) {
+			uint8_t buf[LWS_PRE + 1];
+			enum lws_write_protocol wp = 0;
+
+			if (!rops_write_role_protocol_h2(w, buf + LWS_PRE, 0,
+							 &wp)) {
+				lwsl_info("%s: wsi %p: entering ro long poll\n",
+					  __func__, w);
+				lws_http_mark_immortal(w);
+			} else
+				lwsl_err("%s: wsi %p: failed to set long poll\n",
+						__func__, w);
+			goto next_child;
+		}
+
 		if (lws_callback_as_writeable(w)) {
 			lwsl_info("Closing POLLOUT child (end stream %d)\n",
 				  w->h2.send_END_STREAM);
diff --git a/lib/roles/h2/private-lib-roles-h2.h b/lib/roles/h2/private-lib-roles-h2.h
index e572b1f..ff169ee 100644
--- a/lib/roles/h2/private-lib-roles-h2.h
+++ b/lib/roles/h2/private-lib-roles-h2.h
@@ -328,12 +328,13 @@
 	int my_priority;
 	uint32_t dependent_on;
 
-	unsigned int END_STREAM:1;
-	unsigned int END_HEADERS:1;
-	unsigned int send_END_STREAM:1;
-	unsigned int GOING_AWAY;
-	unsigned int requested_POLLOUT:1;
-	unsigned int skint:1;
+	uint16_t END_STREAM:1;
+	uint16_t END_HEADERS:1;
+	uint16_t send_END_STREAM:1;
+	uint16_t long_poll:1;
+	uint16_t GOING_AWAY;
+	uint16_t requested_POLLOUT:1;
+	uint16_t skint:1;
 
 	uint16_t round_robin_POLLOUT;
 	uint16_t count_POLLOUT_children;
diff --git a/lib/roles/http/server/lejp-conf.c b/lib/roles/http/server/lejp-conf.c
index 2e5a98c..e381940 100644
--- a/lib/roles/http/server/lejp-conf.c
+++ b/lib/roles/http/server/lejp-conf.c
@@ -132,6 +132,7 @@
 	"vhosts[].allow-http-on-https",
 
 	"vhosts[].disable-no-protocol-ws-upgrades",
+	"vhosts[].h2-half-closed-long-poll",
 };
 
 enum lejp_vhost_paths {
@@ -199,6 +200,7 @@
 	LEJPVP_FLAG_ALLOW_HTTP_ON_HTTPS,
 
 	LEJPVP_FLAG_DISABLE_NO_PROTOCOL_WS_UPGRADES,
+	LEJPVP_FLAG_H2_HALF_CLOSED_LONG_POLL,
 };
 
 #define MAX_PLUGIN_DIRS 10
@@ -864,6 +866,11 @@
 		a->reject_ws_with_no_protocol = 1;
 		return 0;
 
+	case LEJPVP_FLAG_H2_HALF_CLOSED_LONG_POLL:
+		set_reset_flag(&a->info->options, ctx->buf,
+				LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL);
+		return 0;
+
 	default:
 		return 0;
 	}
diff --git a/lib/roles/http/server/server.c b/lib/roles/http/server/server.c
index 26168ca..132e962 100644
--- a/lib/roles/http/server/server.c
+++ b/lib/roles/http/server/server.c
@@ -1340,8 +1340,9 @@
 	 * if there is content supposed to be coming,
 	 * put a timeout on it having arrived
 	 */
-	lws_set_timeout(wsi, PENDING_TIMEOUT_HTTP_CONTENT,
-			wsi->context->timeout_secs);
+	if (!wsi->h2_stream_immortal)
+		lws_set_timeout(wsi, PENDING_TIMEOUT_HTTP_CONTENT,
+				wsi->context->timeout_secs);
 #ifdef LWS_WITH_TLS
 	if (wsi->tls.redirect_to_https) {
 		/*
diff --git a/minimal-examples/http-client/minimal-http-client/minimal-http-client.c b/minimal-examples/http-client/minimal-http-client/minimal-http-client.c
index 1dc1be7..2c89d29 100644
--- a/minimal-examples/http-client/minimal-http-client/minimal-http-client.c
+++ b/minimal-examples/http-client/minimal-http-client/minimal-http-client.c
@@ -17,6 +17,9 @@
 #include <signal.h>
 
 static int interrupted, bad = 1, status;
+#if defined(LWS_WITH_HTTP2)
+static int long_poll;
+#endif
 static struct lws *client_wsi;
 
 static int
@@ -35,11 +38,22 @@
 	case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP:
 		status = lws_http_client_http_response(wsi);
 		lwsl_user("Connected with server response: %d\n", status);
+#if defined(LWS_WITH_HTTP2)
+		if (long_poll) {
+			lwsl_user("%s: Client entering long poll mode\n", __func__);
+			lws_h2_client_stream_long_poll_rxonly(wsi);
+		}
+#endif
 		break;
 
 	/* chunks of chunked content, with header removed */
 	case LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ:
 		lwsl_user("RECEIVE_CLIENT_HTTP_READ: read %d\n", (int)len);
+#if defined(LWS_WITH_HTTP2)
+		if (long_poll)
+			lwsl_notice("long poll rx: '%.*s'\n",
+					(int)len, (const char *)in);
+#endif
 #if 0  /* enable to dump the html */
 		{
 			const char *p = in;
@@ -155,8 +169,16 @@
 
 	memset(&i, 0, sizeof i); /* otherwise uninitialized garbage */
 	i.context = context;
-	if (!lws_cmdline_option(argc, argv, "-n"))
+	if (!lws_cmdline_option(argc, argv, "-n")) {
 		i.ssl_connection = LCCSCF_USE_SSL;
+#if defined(LWS_WITH_HTTP2)
+		/* requires h2 */
+		if (lws_cmdline_option(argc, argv, "--long-poll")) {
+			lwsl_user("%s: long poll mode\n", __func__);
+			long_poll = 1;
+		}
+#endif
+	}
 
 	if (lws_cmdline_option(argc, argv, "-l")) {
 		i.port = 7681;
diff --git a/minimal-examples/http-server/minimal-http-server-h2-long-poll/CMakeLists.txt b/minimal-examples/http-server/minimal-http-server-h2-long-poll/CMakeLists.txt
new file mode 100644
index 0000000..4a30f9c
--- /dev/null
+++ b/minimal-examples/http-server/minimal-http-server-h2-long-poll/CMakeLists.txt
@@ -0,0 +1,80 @@
+cmake_minimum_required(VERSION 2.8)
+include(CheckCSourceCompiles)
+
+set(SAMP lws-minimal-http-server-h2-long-poll)
+set(SRCS minimal-http-server.c)
+
+# If we are being built as part of lws, confirm current build config supports
+# reqconfig, else skip building ourselves.
+#
+# If we are being built externally, confirm installed lws was configured to
+# support reqconfig, else error out with a helpful message about the problem.
+#
+MACRO(require_lws_config reqconfig _val result)
+
+	if (DEFINED ${reqconfig})
+	if (${reqconfig})
+		set (rq 1)
+	else()
+		set (rq 0)
+	endif()
+	else()
+		set(rq 0)
+	endif()
+
+	if (${_val} EQUAL ${rq})
+		set(SAME 1)
+	else()
+		set(SAME 0)
+	endif()
+
+	if (LWS_WITH_MINIMAL_EXAMPLES AND NOT ${SAME})
+		if (${_val})
+			message("${SAMP}: skipping as lws being built without ${reqconfig}")
+		else()
+			message("${SAMP}: skipping as lws built with ${reqconfig}")
+		endif()
+		set(${result} 0)
+	else()
+		if (LWS_WITH_MINIMAL_EXAMPLES)
+			set(MET ${SAME})
+		else()
+			CHECK_C_SOURCE_COMPILES("#include <libwebsockets.h>\nint main(void) {\n#if defined(${reqconfig})\n return 0;\n#else\n fail;\n#endif\n return 0;\n}\n" HAS_${reqconfig})
+			if (NOT DEFINED HAS_${reqconfig} OR NOT HAS_${reqconfig})
+				set(HAS_${reqconfig} 0)
+			else()
+				set(HAS_${reqconfig} 1)
+			endif()
+			if ((HAS_${reqconfig} AND ${_val}) OR (NOT HAS_${reqconfig} AND NOT ${_val}))
+				set(MET 1)
+			else()
+				set(MET 0)
+			endif()
+		endif()
+		if (NOT MET)
+			if (${_val})
+				message(FATAL_ERROR "This project requires lws must have been configured with ${reqconfig}")
+			else()
+				message(FATAL_ERROR "Lws configuration of ${reqconfig} is incompatible with this project")
+			endif()
+		endif()
+	
+	endif()
+ENDMACRO()
+
+
+set(requirements 1)
+require_lws_config(LWS_ROLE_H1 1 requirements)
+require_lws_config(LWS_WITH_SERVER 1 requirements)
+require_lws_config(LWS_WITH_HTTP2 1 requirements)
+
+if (requirements)
+	add_executable(${SAMP} ${SRCS})
+
+	if (websockets_shared)
+		target_link_libraries(${SAMP} websockets_shared)
+		add_dependencies(${SAMP} websockets_shared)
+	else()
+		target_link_libraries(${SAMP} websockets)
+	endif()
+endif()
diff --git a/minimal-examples/http-server/minimal-http-server-h2-long-poll/README.md b/minimal-examples/http-server/minimal-http-server-h2-long-poll/README.md
new file mode 100644
index 0000000..cc8794b
--- /dev/null
+++ b/minimal-examples/http-server/minimal-http-server-h2-long-poll/README.md
@@ -0,0 +1,18 @@
+# lws minimal http server
+
+## build
+
+```
+ $ cmake . && make
+```
+
+## usage
+
+```
+ $ ./lws-minimal-http-server
+[2018/03/04 09:30:02:7986] USER: LWS minimal http server | visit http://localhost:7681
+[2018/03/04 09:30:02:7986] NOTICE: Creating Vhost 'default' port 7681, 1 protocols, IPv6 on
+```
+
+Visit http://localhost:7681
+
diff --git a/minimal-examples/http-server/minimal-http-server-h2-long-poll/localhost-100y.cert b/minimal-examples/http-server/minimal-http-server-h2-long-poll/localhost-100y.cert
new file mode 100644
index 0000000..6f372db
--- /dev/null
+++ b/minimal-examples/http-server/minimal-http-server-h2-long-poll/localhost-100y.cert
@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+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
+e2Hl/7M0c/lbZ4FvXgARlex2rkgS0Ka06HE=
+-----END CERTIFICATE-----
diff --git a/minimal-examples/http-server/minimal-http-server-h2-long-poll/localhost-100y.key b/minimal-examples/http-server/minimal-http-server-h2-long-poll/localhost-100y.key
new file mode 100644
index 0000000..148f859
--- /dev/null
+++ b/minimal-examples/http-server/minimal-http-server-h2-long-poll/localhost-100y.key
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+fBrpEY1IATtPq1taBZZogRqI3rOkkPk=
+-----END PRIVATE KEY-----
diff --git a/minimal-examples/http-server/minimal-http-server-h2-long-poll/minimal-http-server.c b/minimal-examples/http-server/minimal-http-server-h2-long-poll/minimal-http-server.c
new file mode 100644
index 0000000..197ce5d
--- /dev/null
+++ b/minimal-examples/http-server/minimal-http-server-h2-long-poll/minimal-http-server.c
@@ -0,0 +1,147 @@
+/*
+ * lws-minimal-http-server-h2-long-poll
+ *
+ * Written in 2010-2019 by Andy Green <andy@warmcat.com>
+ *
+ * This file is made available under the Creative Commons CC0 1.0
+ * Universal Public Domain Dedication.
+ *
+ * This demonstrates an h2 server that supports "long poll"
+ * immortal client connections.  For simplicity it doesn't serve
+ * any regular files, you can add a mount to do it if you want.
+ *
+ * The protocol keeps the long poll h2 stream open, and sends
+ * the time on the stream once per minute.  Normally idle h2
+ * connections are closed by default within 30s, so this demonstrates
+ * the stream and network connection are operating as "immortal"
+ * on both sides.
+ *
+ * See http-client/minimal-http-client-h2-long-poll for the
+ * client example that connects and transitions the stream to the
+ * immortal long poll mode.
+ */
+
+#include <libwebsockets.h>
+#include <string.h>
+#include <signal.h>
+
+static int interrupted;
+
+struct pss {
+	struct lws *wsi;
+	lws_sorted_usec_list_t sul;
+	char pending;
+};
+
+static void
+sul_cb(lws_sorted_usec_list_t *sul)
+{
+	struct pss *pss = (struct pss *)lws_container_of(sul, struct pss, sul);
+
+	pss->pending = 1;
+	lws_callback_on_writable(pss->wsi);
+	/* interval 1min... longer than any normal timeout */
+	lws_sul_schedule(lws_get_context(pss->wsi), 0, &pss->sul, sul_cb,
+				60 * LWS_US_PER_SEC);
+}
+
+static int
+callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user,
+	      void *in, size_t len)
+{
+	struct pss * pss = (struct pss *)user;
+	uint8_t buf[LWS_PRE + LWS_RECOMMENDED_MIN_HEADER_SPACE],
+		*start = &buf[LWS_PRE], *p = start,
+		*end = p + sizeof(buf) - LWS_PRE;
+	int m, n;
+
+	switch (reason) {
+	case LWS_CALLBACK_HTTP:
+		lwsl_user("%s: connect\n", __func__);
+		pss->wsi = wsi;
+
+		if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK,
+				"text/html",
+				LWS_ILLEGAL_HTTP_CONTENT_LEN, /* no content len */
+				&p, end))
+			return 1;
+		if (lws_finalize_write_http_header(wsi, start, &p, end))
+			return 1;
+
+		sul_cb(&pss->sul);
+		return 0;
+
+	case LWS_CALLBACK_CLOSED_HTTP:
+		if (!pss)
+			break;
+		lws_sul_schedule(lws_get_context(wsi), 0, &pss->sul, sul_cb,
+				 LWS_SET_TIMER_USEC_CANCEL);
+		break;
+
+	case LWS_CALLBACK_HTTP_WRITEABLE:
+		if (!pss->pending)
+			break;
+		n = lws_snprintf((char *)p, sizeof(buf) - LWS_PRE, "%llu",
+				 (unsigned long long)lws_now_usecs());
+		m = lws_write(wsi, p, n, LWS_WRITE_HTTP);
+		if (m < n) {
+			lwsl_err("ERROR %d writing to socket\n", n);
+			return -1;
+		}
+		break;
+	default:
+		break;
+	}
+
+	return lws_callback_http_dummy(wsi, reason, user, in, len);
+}
+
+static struct lws_protocols protocols[] = {
+	{ "http", callback_http, sizeof(struct pss), 0 },
+	{ NULL, NULL, 0, 0 } /* terminator */
+};
+
+
+void sigint_handler(int sig)
+{
+	interrupted = 1;
+}
+
+int main(int argc, const char **argv)
+{
+	struct lws_context_creation_info info;
+	struct lws_context *context;
+	const char *p;
+	int n = 0, logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE;
+
+	signal(SIGINT, sigint_handler);
+
+	if ((p = lws_cmdline_option(argc, argv, "-d")))
+		logs = atoi(p);
+
+	lws_set_log_level(logs, NULL);
+	lwsl_user("LWS minimal http server h2 long poll\n");
+
+	memset(&info, 0, sizeof info); /* otherwise uninitialized garbage */
+	info.port = 7681;
+	info.ssl_cert_filepath = "localhost-100y.cert";
+	info.ssl_private_key_filepath = "localhost-100y.key";
+	info.protocols = protocols;
+	info.options =
+		LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT |
+		LWS_SERVER_OPTION_VH_H2_HALF_CLOSED_LONG_POLL |
+		LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE;
+
+	context = lws_create_context(&info);
+	if (!context) {
+		lwsl_err("lws init failed\n");
+		return 1;
+	}
+
+	while (n >= 0 && !interrupted)
+		n = lws_service(context, 0);
+
+	lws_context_destroy(context);
+
+	return 0;
+}