diff --git a/READMEs/README.h2-long-poll.md b/READMEs/README.h2-long-poll.md new file mode 100644 index 000000000..ab8c25fcc --- /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 bc9feffe8..6f741d6ed 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 @@ struct lws_context_creation_info { /**< 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 863655efe..fce21d8c1 100644 --- a/include/libwebsockets/lws-http.h +++ b/include/libwebsockets/lws-http.h @@ -745,6 +745,24 @@ lws_http_headers_detach(struct lws *wsi); LWS_VISIBLE LWS_EXTERN int 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 * diff --git a/lib/core-net/close.c b/lib/core-net/close.c index 7803ccc00..aeac2e40e 100644 --- a/lib/core-net/close.c +++ b/lib/core-net/close.c @@ -254,6 +254,11 @@ __lws_close_free_wsi(struct lws *wsi, enum lws_close_status reason, 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 d6bc0a7a0..bcb73f01b 100644 --- a/lib/core-net/private-lib-core-net.h +++ b/lib/core-net/private-lib-core-net.h @@ -676,8 +676,9 @@ struct lws { 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 @@ lws_prepare_access_log_info(struct lws *wsi, char *uri_ptr, int len, int meth); #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 1609142e6..db026c444 100644 --- a/lib/core-net/wsi-timeout.c +++ b/lib/core-net/wsi-timeout.c @@ -158,6 +158,8 @@ lws_set_timeout(struct lws *wsi, enum pending_timeout reason, int secs) 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 11c14f968..3051c485c 100644 --- a/lib/core-net/wsi.c +++ b/lib/core-net/wsi.c @@ -830,20 +830,69 @@ lws_bind_protocol(struct lws *wsi, const struct lws_protocols *p, 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); - - if (wsi->http2_substream) { - struct lws *nwsi = lws_get_network_wsi(wsi); + lws_http_mark_immortal(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 331646c17..29aa9e746 100644 --- a/lib/roles/h2/http2.c +++ b/lib/roles/h2/http2.c @@ -845,11 +845,9 @@ lws_h2_parse_frame_header(struct lws *wsi) /* 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 @@ lws_h2_parse_frame_header(struct lws *wsi) 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 @@ lws_h2_parser(struct lws *wsi, unsigned char *in, lws_filepos_t inlen, */ 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 @@ lws_read_h2(struct lws *wsi, unsigned char *buf, lws_filepos_t len) 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 dd57ce4f7..de6573912 100644 --- a/lib/roles/h2/ops-h2.c +++ b/lib/roles/h2/ops-h2.c @@ -386,20 +386,22 @@ rops_write_role_protocol_h2(struct lws *wsi, unsigned char *buf, size_t len, /* 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 @@ static int rops_check_upgrades_h2(struct lws *wsi) { #if defined(LWS_ROLE_WS) - struct lws *nwsi; char *p; /* @@ -504,21 +505,16 @@ rops_check_upgrades_h2(struct lws *wsi) 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 @@ rops_close_kill_connection_h2(struct lws *wsi, enum lws_close_status reason) 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 @@ rops_perform_user_POLLOUT_h2(struct lws *wsi) 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 e572b1f16..ff169ee79 100644 --- a/lib/roles/h2/private-lib-roles-h2.h +++ b/lib/roles/h2/private-lib-roles-h2.h @@ -328,12 +328,13 @@ struct _lws_h2_related { 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 2e5a98cbf..e3819407e 100644 --- a/lib/roles/http/server/lejp-conf.c +++ b/lib/roles/http/server/lejp-conf.c @@ -132,6 +132,7 @@ static const char * const paths_vhosts[] = { "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 @@ enum lejp_vhost_paths { 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 @@ lejp_vhosts_cb(struct lejp_ctx *ctx, char reason) 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 26168cae9..132e962fe 100644 --- a/lib/roles/http/server/server.c +++ b/lib/roles/http/server/server.c @@ -1340,8 +1340,9 @@ lws_http_action(struct lws *wsi) * 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 1dc1be741..2c89d29d7 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 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 @@ callback_http(struct lws *wsi, enum lws_callback_reasons reason, 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 @@ int main(int argc, const char **argv) 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 000000000..4a30f9cb2 --- /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 \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 000000000..cc8794b8a --- /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 000000000..6f372db40 --- /dev/null +++ b/minimal-examples/http-server/minimal-http-server-h2-long-poll/localhost-100y.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF5jCCA86gAwIBAgIJANq50IuwPFKgMA0GCSqGSIb3DQEBCwUAMIGGMQswCQYD +VQQGEwJHQjEQMA4GA1UECAwHRXJld2hvbjETMBEGA1UEBwwKQWxsIGFyb3VuZDEb +MBkGA1UECgwSbGlid2Vic29ja2V0cy10ZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3Qx +HzAdBgkqhkiG9w0BCQEWEG5vbmVAaW52YWxpZC5vcmcwIBcNMTgwMzIwMDQxNjA3 +WhgPMjExODAyMjQwNDE2MDdaMIGGMQswCQYDVQQGEwJHQjEQMA4GA1UECAwHRXJl +d2hvbjETMBEGA1UEBwwKQWxsIGFyb3VuZDEbMBkGA1UECgwSbGlid2Vic29ja2V0 +cy10ZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QxHzAdBgkqhkiG9w0BCQEWEG5vbmVA +aW52YWxpZC5vcmcwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCjYtuW +aICCY0tJPubxpIgIL+WWmz/fmK8IQr11Wtee6/IUyUlo5I602mq1qcLhT/kmpoR8 +Di3DAmHKnSWdPWtn1BtXLErLlUiHgZDrZWInmEBjKM1DZf+CvNGZ+EzPgBv5nTek +LWcfI5ZZtoGuIP1Dl/IkNDw8zFz4cpiMe/BFGemyxdHhLrKHSm8Eo+nT734tItnH +KT/m6DSU0xlZ13d6ehLRm7/+Nx47M3XMTRH5qKP/7TTE2s0U6+M0tsGI2zpRi+m6 +jzhNyMBTJ1u58qAe3ZW5/+YAiuZYAB6n5bhUp4oFuB5wYbcBywVR8ujInpF8buWQ +Ujy5N8pSNp7szdYsnLJpvAd0sibrNPjC0FQCNrpNjgJmIK3+mKk4kXX7ZTwefoAz +TK4l2pHNuC53QVc/EF++GBLAxmvCDq9ZpMIYi7OmzkkAKKC9Ue6Ef217LFQCFIBK +Izv9cgi9fwPMLhrKleoVRNsecBsCP569WgJXhUnwf2lon4fEZr3+vRuc9shfqnV0 +nPN1IMSnzXCast7I2fiuRXdIz96KjlGQpP4XfNVA+RGL7aMnWOFIaVrKWLzAtgzo +GMTvP/AuehKXncBJhYtW0ltTioVx+5yTYSAZWl+IssmXjefxJqYi2/7QWmv1QC9p +sNcjTMaBQLN03T1Qelbs7Y27sxdEnNUth4kI+wIDAQABo1MwUTAdBgNVHQ4EFgQU +9mYU23tW2zsomkKTAXarjr2vjuswHwYDVR0jBBgwFoAU9mYU23tW2zsomkKTAXar +jr2vjuswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEANjIBMrow +YNCbhAJdP7dhlhT2RUFRdeRUJD0IxrH/hkvb6myHHnK8nOYezFPjUlmRKUgNEDuA +xbnXZzPdCRNV9V2mShbXvCyiDY7WCQE2Bn44z26O0uWVk+7DNNLH9BnkwUtOnM9P +wtmD9phWexm4q2GnTsiL6Ul6cy0QlTJWKVLEUQQ6yda582e23J1AXqtqFcpfoE34 +H3afEiGy882b+ZBiwkeV+oq6XVF8sFyr9zYrv9CvWTYlkpTQfLTZSsgPdEHYVcjv +xQ2D+XyDR0aRLRlvxUa9dHGFHLICG34Juq5Ai6lM1EsoD8HSsJpMcmrH7MWw2cKk +ujC3rMdFTtte83wF1uuF4FjUC72+SmcQN7A386BC/nk2TTsJawTDzqwOu/VdZv2g +1WpTHlumlClZeP+G/jkSyDwqNnTu1aodDmUa4xZodfhP1HWPwUKFcq8oQr148QYA +AOlbUOJQU7QwRWd1VbnwhDtQWXC92A2w1n/xkZSR1BM/NUSDhkBSUU1WjMbWg6Gg +mnIZLRerQCu1Oozr87rOQqQakPkyt8BUSNK3K42j2qcfhAONdRl8Hq8Qs5pupy+s +8sdCGDlwR3JNCMv6u48OK87F4mcIxhkSefFJUFII25pCGN5WtE4p5l+9cnO1GrIX +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 000000000..148f8598e --- /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 000000000..197ce5d8c --- /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 + * + * 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 +#include +#include + +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; +}