summaryrefslogtreecommitdiff
path: root/thirdparty/libwebsockets/roles/ws/ops-ws.c
diff options
context:
space:
mode:
Diffstat (limited to 'thirdparty/libwebsockets/roles/ws/ops-ws.c')
-rw-r--r--thirdparty/libwebsockets/roles/ws/ops-ws.c1992
1 files changed, 1992 insertions, 0 deletions
diff --git a/thirdparty/libwebsockets/roles/ws/ops-ws.c b/thirdparty/libwebsockets/roles/ws/ops-ws.c
new file mode 100644
index 0000000000..5ddaba9e18
--- /dev/null
+++ b/thirdparty/libwebsockets/roles/ws/ops-ws.c
@@ -0,0 +1,1992 @@
+/*
+ * libwebsockets - small server side websockets and web server implementation
+ *
+ * Copyright (C) 2010-2018 Andy Green <andy@warmcat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation:
+ * version 2.1 of the License.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+#include <core/private.h>
+
+#define LWS_CPYAPP(ptr, str) { strcpy(ptr, str); ptr += strlen(str); }
+
+/*
+ * client-parser.c: lws_ws_client_rx_sm() needs to be roughly kept in
+ * sync with changes here, esp related to ext draining
+ */
+
+int
+lws_ws_rx_sm(struct lws *wsi, char already_processed, unsigned char c)
+{
+ int callback_action = LWS_CALLBACK_RECEIVE;
+ int ret = 0;
+ unsigned short close_code;
+ struct lws_tokens ebuf;
+ unsigned char *pp;
+ int n = 0;
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ int rx_draining_ext = 0;
+ int lin;
+#endif
+
+ ebuf.token = NULL;
+ ebuf.len = 0;
+ if (wsi->socket_is_permanently_unusable)
+ return -1;
+
+ switch (wsi->lws_rx_parse_state) {
+ case LWS_RXPS_NEW:
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ if (wsi->ws->rx_draining_ext) {
+ ebuf.token = NULL;
+ ebuf.len = 0;
+ lws_remove_wsi_from_draining_ext_list(wsi);
+ rx_draining_ext = 1;
+ lwsl_debug("%s: doing draining flow\n", __func__);
+
+ goto drain_extension;
+ }
+#endif
+ switch (wsi->ws->ietf_spec_revision) {
+ case 13:
+ /*
+ * no prepended frame key any more
+ */
+ wsi->ws->all_zero_nonce = 1;
+ goto handle_first;
+
+ default:
+ lwsl_warn("lws_ws_rx_sm: unknown spec version %d\n",
+ wsi->ws->ietf_spec_revision);
+ break;
+ }
+ break;
+ case LWS_RXPS_04_mask_1:
+ wsi->ws->mask[1] = c;
+ if (c)
+ wsi->ws->all_zero_nonce = 0;
+ wsi->lws_rx_parse_state = LWS_RXPS_04_mask_2;
+ break;
+ case LWS_RXPS_04_mask_2:
+ wsi->ws->mask[2] = c;
+ if (c)
+ wsi->ws->all_zero_nonce = 0;
+ wsi->lws_rx_parse_state = LWS_RXPS_04_mask_3;
+ break;
+ case LWS_RXPS_04_mask_3:
+ wsi->ws->mask[3] = c;
+ if (c)
+ wsi->ws->all_zero_nonce = 0;
+
+ /*
+ * start from the zero'th byte in the XOR key buffer since
+ * this is the start of a frame with a new key
+ */
+
+ wsi->ws->mask_idx = 0;
+
+ wsi->lws_rx_parse_state = LWS_RXPS_04_FRAME_HDR_1;
+ break;
+
+ /*
+ * 04 logical framing from the spec (all this is masked when incoming
+ * and has to be unmasked)
+ *
+ * We ignore the possibility of extension data because we don't
+ * negotiate any extensions at the moment.
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-------+-+-------------+-------------------------------+
+ * |F|R|R|R| opcode|R| Payload len | Extended payload length |
+ * |I|S|S|S| (4) |S| (7) | (16/63) |
+ * |N|V|V|V| |V| | (if payload len==126/127) |
+ * | |1|2|3| |4| | |
+ * +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+ * | Extended payload length continued, if payload len == 127 |
+ * + - - - - - - - - - - - - - - - +-------------------------------+
+ * | | Extension data |
+ * +-------------------------------+ - - - - - - - - - - - - - - - +
+ * : :
+ * +---------------------------------------------------------------+
+ * : Application data :
+ * +---------------------------------------------------------------+
+ *
+ * We pass payload through to userland as soon as we get it, ignoring
+ * FIN. It's up to userland to buffer it up if it wants to see a
+ * whole unfragmented block of the original size (which may be up to
+ * 2^63 long!)
+ */
+
+ case LWS_RXPS_04_FRAME_HDR_1:
+handle_first:
+
+ wsi->ws->opcode = c & 0xf;
+ wsi->ws->rsv = c & 0x70;
+ wsi->ws->final = !!((c >> 7) & 1);
+ wsi->ws->defeat_check_utf8 = 0;
+
+ if (((wsi->ws->opcode) & 8) && !wsi->ws->final) {
+ lws_close_reason(wsi, LWS_CLOSE_STATUS_PROTOCOL_ERR,
+ (uint8_t *)"frag ctl", 8);
+ return -1;
+ }
+
+ switch (wsi->ws->opcode) {
+ case LWSWSOPC_TEXT_FRAME:
+ wsi->ws->check_utf8 = lws_check_opt(
+ wsi->context->options,
+ LWS_SERVER_OPTION_VALIDATE_UTF8);
+ /* fallthru */
+ case LWSWSOPC_BINARY_FRAME:
+ if (wsi->ws->opcode == LWSWSOPC_BINARY_FRAME)
+ wsi->ws->check_utf8 = 0;
+ if (wsi->ws->continuation_possible) {
+ lws_close_reason(wsi, LWS_CLOSE_STATUS_PROTOCOL_ERR, (uint8_t *)"bad cont", 8);
+ return -1;
+ }
+ wsi->ws->rsv_first_msg = (c & 0x70);
+ wsi->ws->frame_is_binary =
+ wsi->ws->opcode == LWSWSOPC_BINARY_FRAME;
+ wsi->ws->first_fragment = 1;
+ wsi->ws->continuation_possible = !wsi->ws->final;
+ break;
+ case LWSWSOPC_CONTINUATION:
+ if (!wsi->ws->continuation_possible) {
+ lws_close_reason(wsi, LWS_CLOSE_STATUS_PROTOCOL_ERR, (uint8_t *)"bad cont", 8);
+ return -1;
+ }
+ break;
+ case LWSWSOPC_CLOSE:
+ wsi->ws->check_utf8 = 0;
+ wsi->ws->utf8 = 0;
+ break;
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ case 0xb:
+ case 0xc:
+ case 0xd:
+ case 0xe:
+ case 0xf:
+ lws_close_reason(wsi, LWS_CLOSE_STATUS_PROTOCOL_ERR, (uint8_t *)"bad opc", 7);
+ lwsl_info("illegal opcode\n");
+ return -1;
+ }
+
+ if (wsi->ws->owed_a_fin &&
+ (wsi->ws->opcode == LWSWSOPC_TEXT_FRAME ||
+ wsi->ws->opcode == LWSWSOPC_BINARY_FRAME)) {
+ lwsl_info("hey you owed us a FIN\n");
+ lws_close_reason(wsi, LWS_CLOSE_STATUS_PROTOCOL_ERR, (uint8_t *)"bad fin", 7);
+ return -1;
+ }
+ if ((!(wsi->ws->opcode & 8)) && wsi->ws->final) {
+ wsi->ws->continuation_possible = 0;
+ wsi->ws->owed_a_fin = 0;
+ }
+
+ if (!wsi->ws->final)
+ wsi->ws->owed_a_fin = 1;
+
+ wsi->lws_rx_parse_state = LWS_RXPS_04_FRAME_HDR_LEN;
+ if (wsi->ws->rsv &&
+ (
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ !wsi->ws->count_act_ext ||
+#endif
+ (wsi->ws->rsv & ~0x40))) {
+ lws_close_reason(wsi, LWS_CLOSE_STATUS_PROTOCOL_ERR,
+ (uint8_t *)"rsv bits", 8);
+ return -1;
+ }
+ break;
+
+ case LWS_RXPS_04_FRAME_HDR_LEN:
+
+ wsi->ws->this_frame_masked = !!(c & 0x80);
+
+ switch (c & 0x7f) {
+ case 126:
+ /* control frames are not allowed to have big lengths */
+ if (wsi->ws->opcode & 8)
+ goto illegal_ctl_length;
+
+ wsi->lws_rx_parse_state = LWS_RXPS_04_FRAME_HDR_LEN16_2;
+ break;
+ case 127:
+ /* control frames are not allowed to have big lengths */
+ if (wsi->ws->opcode & 8)
+ goto illegal_ctl_length;
+
+ wsi->lws_rx_parse_state = LWS_RXPS_04_FRAME_HDR_LEN64_8;
+ break;
+ default:
+ wsi->ws->rx_packet_length = c & 0x7f;
+
+
+ if (wsi->ws->this_frame_masked)
+ wsi->lws_rx_parse_state =
+ LWS_RXPS_07_COLLECT_FRAME_KEY_1;
+ else
+ if (wsi->ws->rx_packet_length) {
+ wsi->lws_rx_parse_state =
+ LWS_RXPS_WS_FRAME_PAYLOAD;
+ } else {
+ wsi->lws_rx_parse_state = LWS_RXPS_NEW;
+ goto spill;
+ }
+ break;
+ }
+ break;
+
+ case LWS_RXPS_04_FRAME_HDR_LEN16_2:
+ wsi->ws->rx_packet_length = c << 8;
+ wsi->lws_rx_parse_state = LWS_RXPS_04_FRAME_HDR_LEN16_1;
+ break;
+
+ case LWS_RXPS_04_FRAME_HDR_LEN16_1:
+ wsi->ws->rx_packet_length |= c;
+ if (wsi->ws->this_frame_masked)
+ wsi->lws_rx_parse_state =
+ LWS_RXPS_07_COLLECT_FRAME_KEY_1;
+ else {
+ wsi->lws_rx_parse_state =
+ LWS_RXPS_WS_FRAME_PAYLOAD;
+ }
+ break;
+
+ case LWS_RXPS_04_FRAME_HDR_LEN64_8:
+ if (c & 0x80) {
+ lwsl_warn("b63 of length must be zero\n");
+ /* kill the connection */
+ return -1;
+ }
+#if defined __LP64__
+ wsi->ws->rx_packet_length = ((size_t)c) << 56;
+#else
+ wsi->ws->rx_packet_length = 0;
+#endif
+ wsi->lws_rx_parse_state = LWS_RXPS_04_FRAME_HDR_LEN64_7;
+ break;
+
+ case LWS_RXPS_04_FRAME_HDR_LEN64_7:
+#if defined __LP64__
+ wsi->ws->rx_packet_length |= ((size_t)c) << 48;
+#endif
+ wsi->lws_rx_parse_state = LWS_RXPS_04_FRAME_HDR_LEN64_6;
+ break;
+
+ case LWS_RXPS_04_FRAME_HDR_LEN64_6:
+#if defined __LP64__
+ wsi->ws->rx_packet_length |= ((size_t)c) << 40;
+#endif
+ wsi->lws_rx_parse_state = LWS_RXPS_04_FRAME_HDR_LEN64_5;
+ break;
+
+ case LWS_RXPS_04_FRAME_HDR_LEN64_5:
+#if defined __LP64__
+ wsi->ws->rx_packet_length |= ((size_t)c) << 32;
+#endif
+ wsi->lws_rx_parse_state = LWS_RXPS_04_FRAME_HDR_LEN64_4;
+ break;
+
+ case LWS_RXPS_04_FRAME_HDR_LEN64_4:
+ wsi->ws->rx_packet_length |= ((size_t)c) << 24;
+ wsi->lws_rx_parse_state = LWS_RXPS_04_FRAME_HDR_LEN64_3;
+ break;
+
+ case LWS_RXPS_04_FRAME_HDR_LEN64_3:
+ wsi->ws->rx_packet_length |= ((size_t)c) << 16;
+ wsi->lws_rx_parse_state = LWS_RXPS_04_FRAME_HDR_LEN64_2;
+ break;
+
+ case LWS_RXPS_04_FRAME_HDR_LEN64_2:
+ wsi->ws->rx_packet_length |= ((size_t)c) << 8;
+ wsi->lws_rx_parse_state = LWS_RXPS_04_FRAME_HDR_LEN64_1;
+ break;
+
+ case LWS_RXPS_04_FRAME_HDR_LEN64_1:
+ wsi->ws->rx_packet_length |= ((size_t)c);
+ if (wsi->ws->this_frame_masked)
+ wsi->lws_rx_parse_state =
+ LWS_RXPS_07_COLLECT_FRAME_KEY_1;
+ else
+ wsi->lws_rx_parse_state = LWS_RXPS_WS_FRAME_PAYLOAD;
+ break;
+
+ case LWS_RXPS_07_COLLECT_FRAME_KEY_1:
+ wsi->ws->mask[0] = c;
+ if (c)
+ wsi->ws->all_zero_nonce = 0;
+ wsi->lws_rx_parse_state = LWS_RXPS_07_COLLECT_FRAME_KEY_2;
+ break;
+
+ case LWS_RXPS_07_COLLECT_FRAME_KEY_2:
+ wsi->ws->mask[1] = c;
+ if (c)
+ wsi->ws->all_zero_nonce = 0;
+ wsi->lws_rx_parse_state = LWS_RXPS_07_COLLECT_FRAME_KEY_3;
+ break;
+
+ case LWS_RXPS_07_COLLECT_FRAME_KEY_3:
+ wsi->ws->mask[2] = c;
+ if (c)
+ wsi->ws->all_zero_nonce = 0;
+ wsi->lws_rx_parse_state = LWS_RXPS_07_COLLECT_FRAME_KEY_4;
+ break;
+
+ case LWS_RXPS_07_COLLECT_FRAME_KEY_4:
+ wsi->ws->mask[3] = c;
+ if (c)
+ wsi->ws->all_zero_nonce = 0;
+ wsi->lws_rx_parse_state = LWS_RXPS_WS_FRAME_PAYLOAD;
+ wsi->ws->mask_idx = 0;
+ if (wsi->ws->rx_packet_length == 0) {
+ wsi->lws_rx_parse_state = LWS_RXPS_NEW;
+ goto spill;
+ }
+ break;
+
+
+ case LWS_RXPS_WS_FRAME_PAYLOAD:
+ assert(wsi->ws->rx_ubuf);
+
+ if (wsi->ws->rx_ubuf_head + LWS_PRE >= wsi->ws->rx_ubuf_alloc) {
+ lwsl_err("Attempted overflow \n");
+ return -1;
+ }
+ if (!(already_processed & ALREADY_PROCESSED_IGNORE_CHAR)) {
+ if (wsi->ws->all_zero_nonce)
+ wsi->ws->rx_ubuf[LWS_PRE + (wsi->ws->rx_ubuf_head++)] =
+ c;
+ else
+ wsi->ws->rx_ubuf[LWS_PRE + (wsi->ws->rx_ubuf_head++)] =
+ c ^ wsi->ws->mask[(wsi->ws->mask_idx++) & 3];
+
+ --wsi->ws->rx_packet_length;
+ }
+
+ if (!wsi->ws->rx_packet_length) {
+ lwsl_debug("%s: ws fragment length exhausted\n", __func__);
+ /* spill because we have the whole frame */
+ wsi->lws_rx_parse_state = LWS_RXPS_NEW;
+ goto spill;
+ }
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ if (wsi->ws->rx_draining_ext) {
+ lwsl_debug("%s: UNTIL_EXHAUSTED draining\n", __func__);
+ goto drain_extension;
+ }
+#endif
+ /*
+ * if there's no protocol max frame size given, we are
+ * supposed to default to context->pt_serv_buf_size
+ */
+ if (!wsi->protocol->rx_buffer_size &&
+ wsi->ws->rx_ubuf_head != wsi->context->pt_serv_buf_size)
+ break;
+
+ if (wsi->protocol->rx_buffer_size &&
+ wsi->ws->rx_ubuf_head != wsi->protocol->rx_buffer_size)
+ break;
+
+ /* spill because we filled our rx buffer */
+spill:
+ /*
+ * is this frame a control packet we should take care of at this
+ * layer? If so service it and hide it from the user callback
+ */
+
+ lwsl_parser("spill on %s\n", wsi->protocol->name);
+
+ switch (wsi->ws->opcode) {
+ case LWSWSOPC_CLOSE:
+
+ if (wsi->ws->peer_has_sent_close)
+ break;
+
+ wsi->ws->peer_has_sent_close = 1;
+
+ pp = (unsigned char *)&wsi->ws->rx_ubuf[LWS_PRE];
+ if (lws_check_opt(wsi->context->options,
+ LWS_SERVER_OPTION_VALIDATE_UTF8) &&
+ wsi->ws->rx_ubuf_head > 2 &&
+ lws_check_utf8(&wsi->ws->utf8, pp + 2,
+ wsi->ws->rx_ubuf_head - 2))
+ goto utf8_fail;
+
+ /* is this an acknowledgment of our close? */
+ if (lwsi_state(wsi) == LRS_AWAITING_CLOSE_ACK) {
+ /*
+ * fine he has told us he is closing too, let's
+ * finish our close
+ */
+ lwsl_parser("seen client close ack\n");
+ return -1;
+ }
+ if (lwsi_state(wsi) == LRS_RETURNED_CLOSE)
+ /* if he sends us 2 CLOSE, kill him */
+ return -1;
+
+ if (lws_partial_buffered(wsi)) {
+ /*
+ * if we're in the middle of something,
+ * we can't do a normal close response and
+ * have to just close our end.
+ */
+ wsi->socket_is_permanently_unusable = 1;
+ lwsl_parser("Closing on peer close due to Pending tx\n");
+ return -1;
+ }
+
+ if (wsi->ws->rx_ubuf_head >= 2) {
+ close_code = (pp[0] << 8) | pp[1];
+ if (close_code < 1000 ||
+ close_code == 1004 ||
+ close_code == 1005 ||
+ close_code == 1006 ||
+ close_code == 1012 ||
+ close_code == 1013 ||
+ close_code == 1014 ||
+ close_code == 1015 ||
+ (close_code >= 1016 && close_code < 3000)
+ ) {
+ pp[0] = (LWS_CLOSE_STATUS_PROTOCOL_ERR >> 8) & 0xff;
+ pp[1] = LWS_CLOSE_STATUS_PROTOCOL_ERR & 0xff;
+ }
+ }
+
+ if (user_callback_handle_rxflow(
+ wsi->protocol->callback, wsi,
+ LWS_CALLBACK_WS_PEER_INITIATED_CLOSE,
+ wsi->user_space,
+ &wsi->ws->rx_ubuf[LWS_PRE],
+ wsi->ws->rx_ubuf_head))
+ return -1;
+
+ lwsl_parser("server sees client close packet\n");
+ lwsi_set_state(wsi, LRS_RETURNED_CLOSE);
+ /* deal with the close packet contents as a PONG */
+ wsi->ws->payload_is_close = 1;
+ goto process_as_ping;
+
+ case LWSWSOPC_PING:
+ lwsl_info("received %d byte ping, sending pong\n",
+ wsi->ws->rx_ubuf_head);
+
+ if (wsi->ws->ping_pending_flag) {
+ /*
+ * there is already a pending ping payload
+ * we should just log and drop
+ */
+ lwsl_parser("DROP PING since one pending\n");
+ goto ping_drop;
+ }
+process_as_ping:
+ /* control packets can only be < 128 bytes long */
+ if (wsi->ws->rx_ubuf_head > 128 - 3) {
+ lwsl_parser("DROP PING payload too large\n");
+ goto ping_drop;
+ }
+
+ /* stash the pong payload */
+ memcpy(wsi->ws->ping_payload_buf + LWS_PRE,
+ &wsi->ws->rx_ubuf[LWS_PRE],
+ wsi->ws->rx_ubuf_head);
+
+ wsi->ws->ping_payload_len = wsi->ws->rx_ubuf_head;
+ wsi->ws->ping_pending_flag = 1;
+
+ /* get it sent as soon as possible */
+ lws_callback_on_writable(wsi);
+ping_drop:
+ wsi->ws->rx_ubuf_head = 0;
+ return 0;
+
+ case LWSWSOPC_PONG:
+ lwsl_info("received pong\n");
+ lwsl_hexdump(&wsi->ws->rx_ubuf[LWS_PRE],
+ wsi->ws->rx_ubuf_head);
+
+ if (wsi->pending_timeout ==
+ PENDING_TIMEOUT_WS_PONG_CHECK_GET_PONG) {
+ lwsl_info("received expected PONG on wsi %p\n",
+ wsi);
+ lws_set_timeout(wsi, NO_PENDING_TIMEOUT, 0);
+ }
+
+ /* issue it */
+ callback_action = LWS_CALLBACK_RECEIVE_PONG;
+ break;
+
+ case LWSWSOPC_TEXT_FRAME:
+ case LWSWSOPC_BINARY_FRAME:
+ case LWSWSOPC_CONTINUATION:
+ break;
+
+ default:
+ lwsl_parser("unknown opc %x\n", wsi->ws->opcode);
+
+ return -1;
+ }
+
+ /*
+ * No it's real payload, pass it up to the user callback.
+ * It's nicely buffered with the pre-padding taken care of
+ * so it can be sent straight out again using lws_write
+ */
+
+ ebuf.token = &wsi->ws->rx_ubuf[LWS_PRE];
+ ebuf.len = wsi->ws->rx_ubuf_head;
+
+ if (wsi->ws->opcode == LWSWSOPC_PONG && !ebuf.len)
+ goto already_done;
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+drain_extension:
+#endif
+ // lwsl_notice("%s: passing %d to ext\n", __func__, ebuf.len);
+
+ if (lwsi_state(wsi) == LRS_RETURNED_CLOSE ||
+ lwsi_state(wsi) == LRS_AWAITING_CLOSE_ACK)
+ goto already_done;
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ lin = ebuf.len;
+ //if (lin)
+ // lwsl_hexdump_notice(ebuf.token, ebuf.len);
+ n = lws_ext_cb_active(wsi, LWS_EXT_CB_PAYLOAD_RX, &ebuf, 0);
+ lwsl_debug("%s: ext says %d / ebuf.len %d\n", __func__, n, ebuf.len);
+ if (wsi->ws->rx_draining_ext)
+ already_processed &= ~ALREADY_PROCESSED_NO_CB;
+#endif
+ /*
+ * ebuf may be pointing somewhere completely different now,
+ * it's the output
+ */
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ if (n < 0) {
+ /*
+ * we may rely on this to get RX, just drop connection
+ */
+ wsi->socket_is_permanently_unusable = 1;
+ return -1;
+ }
+#endif
+ if (
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ rx_draining_ext &&
+#endif
+ ebuf.len == 0)
+ goto already_done;
+
+ if (
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ n &&
+#endif
+ ebuf.len)
+ /* extension had more... main loop will come back */
+ lws_add_wsi_to_draining_ext_list(wsi);
+ else
+ lws_remove_wsi_from_draining_ext_list(wsi);
+
+ if (wsi->ws->check_utf8 && !wsi->ws->defeat_check_utf8) {
+ if (lws_check_utf8(&wsi->ws->utf8,
+ (unsigned char *)ebuf.token,
+ ebuf.len)) {
+ lws_close_reason(wsi,
+ LWS_CLOSE_STATUS_INVALID_PAYLOAD,
+ (uint8_t *)"bad utf8", 8);
+ goto utf8_fail;
+ }
+
+ /* we are ending partway through utf-8 character? */
+ if (!wsi->ws->rx_packet_length && wsi->ws->final &&
+ wsi->ws->utf8 && !n) {
+ lwsl_info("FINAL utf8 error\n");
+ lws_close_reason(wsi,
+ LWS_CLOSE_STATUS_INVALID_PAYLOAD,
+ (uint8_t *)"partial utf8", 12);
+utf8_fail:
+ lwsl_notice("utf8 error\n");
+ lwsl_hexdump_notice(ebuf.token, ebuf.len);
+
+ return -1;
+ }
+ }
+
+ if (!wsi->wsistate_pre_close && (ebuf.len >= 0 ||
+ callback_action == LWS_CALLBACK_RECEIVE_PONG)) {
+ if (ebuf.len)
+ ebuf.token[ebuf.len] = '\0';
+
+ if (wsi->protocol->callback &&
+ !(already_processed & ALREADY_PROCESSED_NO_CB)) {
+ if (callback_action == LWS_CALLBACK_RECEIVE_PONG)
+ lwsl_info("Doing pong callback\n");
+
+ ret = user_callback_handle_rxflow(
+ wsi->protocol->callback,
+ wsi, (enum lws_callback_reasons)
+ callback_action,
+ wsi->user_space,
+ ebuf.token,
+ ebuf.len);
+ }
+ wsi->ws->first_fragment = 0;
+ }
+
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ if (!lin)
+ break;
+#endif
+
+already_done:
+ wsi->ws->rx_ubuf_head = 0;
+ break;
+ }
+
+ return ret;
+
+illegal_ctl_length:
+
+ lwsl_warn("Control frame with xtended length is illegal\n");
+ /* kill the connection */
+ return -1;
+}
+
+
+LWS_VISIBLE size_t
+lws_remaining_packet_payload(struct lws *wsi)
+{
+ return wsi->ws->rx_packet_length;
+}
+
+LWS_VISIBLE int lws_frame_is_binary(struct lws *wsi)
+{
+ return wsi->ws->frame_is_binary;
+}
+
+void
+lws_add_wsi_to_draining_ext_list(struct lws *wsi)
+{
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi];
+
+ if (wsi->ws->rx_draining_ext)
+ return;
+
+ lwsl_debug("%s: RX EXT DRAINING: Adding to list\n", __func__);
+
+ wsi->ws->rx_draining_ext = 1;
+ wsi->ws->rx_draining_ext_list = pt->ws.rx_draining_ext_list;
+ pt->ws.rx_draining_ext_list = wsi;
+#endif
+}
+
+void
+lws_remove_wsi_from_draining_ext_list(struct lws *wsi)
+{
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi];
+ struct lws **w = &pt->ws.rx_draining_ext_list;
+
+ if (!wsi->ws->rx_draining_ext)
+ return;
+
+ lwsl_debug("%s: RX EXT DRAINING: Removing from list\n", __func__);
+
+ wsi->ws->rx_draining_ext = 0;
+
+ /* remove us from context draining ext list */
+ while (*w) {
+ if (*w == wsi) {
+ /* if us, point it instead to who we were pointing to */
+ *w = wsi->ws->rx_draining_ext_list;
+ break;
+ }
+ w = &((*w)->ws->rx_draining_ext_list);
+ }
+ wsi->ws->rx_draining_ext_list = NULL;
+#endif
+}
+
+LWS_EXTERN void
+lws_restart_ws_ping_pong_timer(struct lws *wsi)
+{
+ if (!wsi->context->ws_ping_pong_interval ||
+ !lwsi_role_ws(wsi))
+ return;
+
+ wsi->ws->time_next_ping_check = (time_t)lws_now_secs();
+}
+
+static int
+lws_0405_frame_mask_generate(struct lws *wsi)
+{
+ int n;
+ /* fetch the per-frame nonce */
+
+ n = lws_get_random(lws_get_context(wsi), wsi->ws->mask, 4);
+ if (n != 4) {
+ lwsl_parser("Unable to read from random device %s %d\n",
+ SYSTEM_RANDOM_FILEPATH, n);
+ return 1;
+ }
+
+ /* start masking from first byte of masking key buffer */
+ wsi->ws->mask_idx = 0;
+
+ return 0;
+}
+
+int
+lws_server_init_wsi_for_ws(struct lws *wsi)
+{
+ int n;
+
+ lwsi_set_state(wsi, LRS_ESTABLISHED);
+ lws_restart_ws_ping_pong_timer(wsi);
+
+ /*
+ * create the frame buffer for this connection according to the
+ * size mentioned in the protocol definition. If 0 there, use
+ * a big default for compatibility
+ */
+
+ n = (int)wsi->protocol->rx_buffer_size;
+ if (!n)
+ n = wsi->context->pt_serv_buf_size;
+ n += LWS_PRE;
+ wsi->ws->rx_ubuf = lws_malloc(n + 4 /* 0x0000ffff zlib */, "rx_ubuf");
+ if (!wsi->ws->rx_ubuf) {
+ lwsl_err("Out of Mem allocating rx buffer %d\n", n);
+ return 1;
+ }
+ wsi->ws->rx_ubuf_alloc = n;
+ lwsl_debug("Allocating RX buffer %d\n", n);
+
+#if !defined(LWS_WITH_ESP32)
+ if (!wsi->parent_carries_io &&
+ !wsi->h2_stream_carries_ws)
+ if (setsockopt(wsi->desc.sockfd, SOL_SOCKET, SO_SNDBUF,
+ (const char *)&n, sizeof n)) {
+ lwsl_warn("Failed to set SNDBUF to %d", n);
+ return 1;
+ }
+#endif
+
+ /* notify user code that we're ready to roll */
+
+ if (wsi->protocol->callback)
+ if (wsi->protocol->callback(wsi, LWS_CALLBACK_ESTABLISHED,
+ wsi->user_space,
+#ifdef LWS_WITH_TLS
+ wsi->tls.ssl,
+#else
+ NULL,
+#endif
+ wsi->h2_stream_carries_ws))
+ return 1;
+
+ lwsl_debug("ws established\n");
+
+ return 0;
+}
+
+
+
+LWS_VISIBLE int
+lws_is_final_fragment(struct lws *wsi)
+{
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ lwsl_debug("%s: final %d, rx pk length %ld, draining %ld\n", __func__,
+ wsi->ws->final, (long)wsi->ws->rx_packet_length,
+ (long)wsi->ws->rx_draining_ext);
+ return wsi->ws->final && !wsi->ws->rx_packet_length &&
+ !wsi->ws->rx_draining_ext;
+#else
+ return wsi->ws->final && !wsi->ws->rx_packet_length;
+#endif
+}
+
+LWS_VISIBLE int
+lws_is_first_fragment(struct lws *wsi)
+{
+ return wsi->ws->first_fragment;
+}
+
+LWS_VISIBLE unsigned char
+lws_get_reserved_bits(struct lws *wsi)
+{
+ return wsi->ws->rsv;
+}
+
+LWS_VISIBLE LWS_EXTERN int
+lws_get_close_length(struct lws *wsi)
+{
+ return wsi->ws->close_in_ping_buffer_len;
+}
+
+LWS_VISIBLE LWS_EXTERN unsigned char *
+lws_get_close_payload(struct lws *wsi)
+{
+ return &wsi->ws->ping_payload_buf[LWS_PRE];
+}
+
+LWS_VISIBLE LWS_EXTERN void
+lws_close_reason(struct lws *wsi, enum lws_close_status status,
+ unsigned char *buf, size_t len)
+{
+ unsigned char *p, *start;
+ int budget = sizeof(wsi->ws->ping_payload_buf) - LWS_PRE;
+
+ assert(lwsi_role_ws(wsi));
+
+ start = p = &wsi->ws->ping_payload_buf[LWS_PRE];
+
+ *p++ = (((int)status) >> 8) & 0xff;
+ *p++ = ((int)status) & 0xff;
+
+ if (buf)
+ while (len-- && p < start + budget)
+ *p++ = *buf++;
+
+ wsi->ws->close_in_ping_buffer_len = lws_ptr_diff(p, start);
+}
+
+static int
+lws_is_ws_with_ext(struct lws *wsi)
+{
+#if defined(LWS_WITHOUT_EXTENSIONS)
+ return 0;
+#else
+ return lwsi_role_ws(wsi) && !!wsi->ws->count_act_ext;
+#endif
+}
+
+static int
+rops_handle_POLLIN_ws(struct lws_context_per_thread *pt, struct lws *wsi,
+ struct lws_pollfd *pollfd)
+{
+ struct lws_tokens ebuf;
+ unsigned int pending = 0;
+ char buffered = 0;
+ int n = 0, m;
+#if defined(LWS_WITH_HTTP2)
+ struct lws *wsi1;
+#endif
+
+ if (!wsi->ws) {
+ lwsl_err("ws role wsi with no ws\n");
+ return 1;
+ }
+
+ // lwsl_notice("%s: %s\n", __func__, wsi->protocol->name);
+
+ //lwsl_info("%s: wsistate 0x%x, pollout %d\n", __func__,
+ // wsi->wsistate, pollfd->revents & LWS_POLLOUT);
+
+ /*
+ * something went wrong with parsing the handshake, and
+ * we ended up back in the event loop without completing it
+ */
+ if (lwsi_state(wsi) == LRS_PRE_WS_SERVING_ACCEPT) {
+ wsi->socket_is_permanently_unusable = 1;
+ return LWS_HPI_RET_PLEASE_CLOSE_ME;
+ }
+
+ ebuf.token = NULL;
+ ebuf.len = 0;
+
+ if (lwsi_state(wsi) == LRS_WAITING_CONNECT) {
+#if !defined(LWS_NO_CLIENT)
+ if ((pollfd->revents & LWS_POLLOUT) &&
+ lws_handle_POLLOUT_event(wsi, pollfd)) {
+ lwsl_debug("POLLOUT event closed it\n");
+ return LWS_HPI_RET_PLEASE_CLOSE_ME;
+ }
+
+ n = lws_client_socket_service(wsi, pollfd, NULL);
+ if (n)
+ return LWS_HPI_RET_WSI_ALREADY_DIED;
+#endif
+ return LWS_HPI_RET_HANDLED;
+ }
+
+ //lwsl_notice("%s: wsi->ws->tx_draining_ext %d revents 0x%x 0x%x %d\n", __func__, wsi->ws->tx_draining_ext, pollfd->revents, wsi->wsistate, lwsi_state_can_handle_POLLOUT(wsi));
+
+ /* 1: something requested a callback when it was OK to write */
+
+ if ((pollfd->revents & LWS_POLLOUT) &&
+ lwsi_state_can_handle_POLLOUT(wsi) &&
+ lws_handle_POLLOUT_event(wsi, pollfd)) {
+ if (lwsi_state(wsi) == LRS_RETURNED_CLOSE)
+ lwsi_set_state(wsi, LRS_FLUSHING_BEFORE_CLOSE);
+
+ return LWS_HPI_RET_PLEASE_CLOSE_ME;
+ }
+
+ if (lwsi_state(wsi) == LRS_RETURNED_CLOSE ||
+ lwsi_state(wsi) == LRS_WAITING_TO_SEND_CLOSE) {
+ /*
+ * we stopped caring about anything except control
+ * packets. Force flow control off, defeat tx
+ * draining.
+ */
+ lws_rx_flow_control(wsi, 1);
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ if (wsi->ws)
+ wsi->ws->tx_draining_ext = 0;
+#endif
+ }
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ if (wsi->ws->tx_draining_ext)
+ /*
+ * We cannot deal with new RX until the TX ext path has
+ * been drained. It's because new rx will, eg, crap on
+ * the wsi rx buf that may be needed to retain state.
+ *
+ * TX ext drain path MUST go through event loop to avoid
+ * blocking.
+ */
+ return LWS_HPI_RET_HANDLED;
+#endif
+ if (lws_is_flowcontrolled(wsi)) {
+ /* We cannot deal with any kind of new RX because we are
+ * RX-flowcontrolled.
+ */
+ lwsl_info("flowcontrolled\n");
+ return LWS_HPI_RET_HANDLED;
+ }
+
+#if defined(LWS_WITH_HTTP2)
+ if (wsi->http2_substream || wsi->upgraded_to_http2) {
+ wsi1 = lws_get_network_wsi(wsi);
+ if (wsi1 && wsi1->trunc_len)
+ /* We cannot deal with any kind of new RX
+ * because we are dealing with a partial send
+ * (new RX may trigger new http_action() that
+ * expect to be able to send)
+ */
+ return LWS_HPI_RET_HANDLED;
+ }
+#endif
+
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ /* 2: RX Extension needs to be drained
+ */
+
+ if (wsi->ws->rx_draining_ext) {
+
+ lwsl_debug("%s: RX EXT DRAINING: Service\n", __func__);
+#ifndef LWS_NO_CLIENT
+ if (lwsi_role_client(wsi)) {
+ n = lws_ws_client_rx_sm(wsi, 0);
+ if (n < 0)
+ /* we closed wsi */
+ return LWS_HPI_RET_PLEASE_CLOSE_ME;
+ } else
+#endif
+ n = lws_ws_rx_sm(wsi, ALREADY_PROCESSED_IGNORE_CHAR, 0);
+
+ return LWS_HPI_RET_HANDLED;
+ }
+
+ if (wsi->ws->rx_draining_ext)
+ /*
+ * We have RX EXT content to drain, but can't do it
+ * right now. That means we cannot do anything lower
+ * priority either.
+ */
+ return LWS_HPI_RET_HANDLED;
+#endif
+
+ /* 3: buflist needs to be drained
+ */
+read:
+ //lws_buflist_describe(&wsi->buflist, wsi);
+ ebuf.len = (int)lws_buflist_next_segment_len(&wsi->buflist,
+ (uint8_t **)&ebuf.token);
+ if (ebuf.len) {
+ lwsl_info("draining buflist (len %d)\n", ebuf.len);
+ buffered = 1;
+ goto drain;
+ }
+
+ if (!(pollfd->revents & pollfd->events & LWS_POLLIN) && !wsi->http.ah)
+ return LWS_HPI_RET_HANDLED;
+
+ if (lws_is_flowcontrolled(wsi)) {
+ lwsl_info("%s: %p should be rxflow (bm 0x%x)..\n",
+ __func__, wsi, wsi->rxflow_bitmap);
+ return LWS_HPI_RET_HANDLED;
+ }
+
+ if (!(lwsi_role_client(wsi) &&
+ (lwsi_state(wsi) != LRS_ESTABLISHED &&
+ lwsi_state(wsi) != LRS_AWAITING_CLOSE_ACK &&
+ lwsi_state(wsi) != LRS_H2_WAITING_TO_SEND_HEADERS))) {
+ /*
+ * In case we are going to react to this rx by scheduling
+ * writes, we need to restrict the amount of rx to the size
+ * the protocol reported for rx buffer.
+ *
+ * Otherwise we get a situation we have to absorb possibly a
+ * lot of reads before we get a chance to drain them by writing
+ * them, eg, with echo type tests in autobahn.
+ */
+
+ buffered = 0;
+ ebuf.token = (char *)pt->serv_buf;
+ if (lwsi_role_ws(wsi))
+ ebuf.len = wsi->ws->rx_ubuf_alloc;
+ else
+ ebuf.len = wsi->context->pt_serv_buf_size;
+
+ if ((unsigned int)ebuf.len > wsi->context->pt_serv_buf_size)
+ ebuf.len = wsi->context->pt_serv_buf_size;
+
+ if ((int)pending > ebuf.len)
+ pending = ebuf.len;
+
+ ebuf.len = lws_ssl_capable_read(wsi, (uint8_t *)ebuf.token,
+ pending ? (int)pending :
+ ebuf.len);
+ switch (ebuf.len) {
+ case 0:
+ lwsl_info("%s: zero length read\n",
+ __func__);
+ return LWS_HPI_RET_PLEASE_CLOSE_ME;
+ case LWS_SSL_CAPABLE_MORE_SERVICE:
+ lwsl_info("SSL Capable more service\n");
+ return LWS_HPI_RET_HANDLED;
+ case LWS_SSL_CAPABLE_ERROR:
+ lwsl_info("%s: LWS_SSL_CAPABLE_ERROR\n",
+ __func__);
+ return LWS_HPI_RET_PLEASE_CLOSE_ME;
+ }
+ // lwsl_notice("Actual RX %d\n", ebuf.len);
+
+ lws_restart_ws_ping_pong_timer(wsi);
+
+ /*
+ * coverity thinks ssl_capable_read() may read over
+ * 2GB. Dissuade it...
+ */
+ ebuf.len &= 0x7fffffff;
+ }
+
+drain:
+
+ /*
+ * give any active extensions a chance to munge the buffer
+ * before parse. We pass in a pointer to an lws_tokens struct
+ * prepared with the default buffer and content length that's in
+ * there. Rather than rewrite the default buffer, extensions
+ * that expect to grow the buffer can adapt .token to
+ * point to their own per-connection buffer in the extension
+ * user allocation. By default with no extensions or no
+ * extension callback handling, just the normal input buffer is
+ * used then so it is efficient.
+ */
+ m = 0;
+ do {
+
+ /* service incoming data */
+ //lws_buflist_describe(&wsi->buflist, wsi);
+ if (ebuf.len) {
+#if defined(LWS_ROLE_H2)
+ if (lwsi_role_h2(wsi) && lwsi_state(wsi) != LRS_BODY)
+ n = lws_read_h2(wsi, (unsigned char *)ebuf.token,
+ ebuf.len);
+ else
+#endif
+ n = lws_read_h1(wsi, (unsigned char *)ebuf.token,
+ ebuf.len);
+
+ if (n < 0) {
+ /* we closed wsi */
+ n = 0;
+ return LWS_HPI_RET_WSI_ALREADY_DIED;
+ }
+ //lws_buflist_describe(&wsi->buflist, wsi);
+ //lwsl_notice("%s: consuming %d / %d\n", __func__, n, ebuf.len);
+ if (lws_buflist_aware_consume(wsi, &ebuf, n, buffered))
+ return LWS_HPI_RET_PLEASE_CLOSE_ME;
+ }
+
+ ebuf.token = NULL;
+ ebuf.len = 0;
+ } while (m);
+
+ if (wsi->http.ah
+#if !defined(LWS_NO_CLIENT)
+ && !wsi->client_h2_alpn
+#endif
+ ) {
+ lwsl_info("%s: %p: detaching ah\n", __func__, wsi);
+ lws_header_table_detach(wsi, 0);
+ }
+
+ pending = lws_ssl_pending(wsi);
+ if (pending) {
+ if (lws_is_ws_with_ext(wsi))
+ pending = pending > wsi->ws->rx_ubuf_alloc ?
+ wsi->ws->rx_ubuf_alloc : pending;
+ else
+ pending = pending > wsi->context->pt_serv_buf_size ?
+ wsi->context->pt_serv_buf_size : pending;
+ goto read;
+ }
+
+ if (buffered && /* were draining, now nothing left */
+ !lws_buflist_next_segment_len(&wsi->buflist, NULL)) {
+ lwsl_info("%s: %p flow buf: drained\n", __func__, wsi);
+ /* having drained the rxflow buffer, can rearm POLLIN */
+#ifdef LWS_NO_SERVER
+ n =
+#endif
+ __lws_rx_flow_control(wsi);
+ /* n ignored, needed for NO_SERVER case */
+ }
+
+ /* n = 0 */
+ return LWS_HPI_RET_HANDLED;
+}
+
+
+int rops_handle_POLLOUT_ws(struct lws *wsi)
+{
+ int write_type = LWS_WRITE_PONG;
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ struct lws_tokens ebuf;
+ int ret, m;
+#endif
+ int n;
+
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ lwsl_debug("%s: %s: wsi->ws->tx_draining_ext %d\n", __func__,
+ wsi->protocol->name, wsi->ws->tx_draining_ext);
+#endif
+
+ /* Priority 3: pending control packets (pong or close)
+ *
+ * 3a: close notification packet requested from close api
+ */
+
+ if (lwsi_state(wsi) == LRS_WAITING_TO_SEND_CLOSE) {
+ lwsl_debug("sending close packet\n");
+ lwsl_hexdump_debug(&wsi->ws->ping_payload_buf[LWS_PRE],
+ wsi->ws->close_in_ping_buffer_len);
+ wsi->waiting_to_send_close_frame = 0;
+ n = lws_write(wsi, &wsi->ws->ping_payload_buf[LWS_PRE],
+ wsi->ws->close_in_ping_buffer_len,
+ LWS_WRITE_CLOSE);
+ if (n >= 0) {
+ if (wsi->close_needs_ack) {
+ lwsi_set_state(wsi, LRS_AWAITING_CLOSE_ACK);
+ lws_set_timeout(wsi, PENDING_TIMEOUT_CLOSE_ACK, 5);
+ lwsl_debug("sent close indication, awaiting ack\n");
+
+ return LWS_HP_RET_BAIL_OK;
+ }
+ wsi->close_needs_ack = 0;
+ lwsi_set_state(wsi, LRS_RETURNED_CLOSE);
+ }
+
+ return LWS_HP_RET_BAIL_DIE;
+ }
+
+ /* else, the send failed and we should just hang up */
+
+ if ((lwsi_role_ws(wsi) && wsi->ws->ping_pending_flag) ||
+ (lwsi_state(wsi) == LRS_RETURNED_CLOSE &&
+ wsi->ws->payload_is_close)) {
+
+ if (wsi->ws->payload_is_close)
+ write_type = LWS_WRITE_CLOSE;
+ else {
+ if (wsi->wsistate_pre_close) {
+ /* we started close flow, forget pong */
+ wsi->ws->ping_pending_flag = 0;
+ return LWS_HP_RET_BAIL_OK;
+ }
+ lwsl_info("issuing pong %d on wsi %p\n", wsi->ws->ping_payload_len, wsi);
+ }
+
+ n = lws_write(wsi, &wsi->ws->ping_payload_buf[LWS_PRE],
+ wsi->ws->ping_payload_len, write_type);
+ if (n < 0)
+ return LWS_HP_RET_BAIL_DIE;
+
+ /* well he is sent, mark him done */
+ wsi->ws->ping_pending_flag = 0;
+ if (wsi->ws->payload_is_close) {
+ // assert(0);
+ /* oh... a close frame was it... then we are done */
+ return LWS_HP_RET_BAIL_DIE;
+ }
+
+ /* otherwise for PING, leave POLLOUT active either way */
+ return LWS_HP_RET_BAIL_OK;
+ }
+
+ if (lwsi_role_client(wsi) && !wsi->socket_is_permanently_unusable &&
+ wsi->ws->send_check_ping) {
+
+ lwsl_info("issuing ping on wsi %p\n", wsi);
+ wsi->ws->send_check_ping = 0;
+ n = lws_write(wsi, &wsi->ws->ping_payload_buf[LWS_PRE],
+ 0, LWS_WRITE_PING);
+ if (n < 0)
+ return LWS_HP_RET_BAIL_DIE;
+
+ /*
+ * we apparently were able to send the PING in a reasonable time
+ * now reset the clock on our peer to be able to send the
+ * PONG in a reasonable time.
+ */
+
+ lws_set_timeout(wsi, PENDING_TIMEOUT_WS_PONG_CHECK_GET_PONG,
+ wsi->context->timeout_secs);
+
+ return LWS_HP_RET_BAIL_OK;
+ }
+
+ /* Priority 4: if we are closing, not allowed to send more data frags
+ * which means user callback or tx ext flush banned now
+ */
+ if (lwsi_state(wsi) == LRS_RETURNED_CLOSE)
+ return LWS_HP_RET_USER_SERVICE;
+
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ /* Priority 5: Tx path extension with more to send
+ *
+ * These are handled as new fragments each time around
+ * So while we must block new writeable callback to enforce
+ * payload ordering, but since they are always complete
+ * fragments control packets can interleave OK.
+ */
+ if (lwsi_role_client(wsi) && wsi->ws->tx_draining_ext) {
+ lwsl_ext("SERVICING TX EXT DRAINING\n");
+ if (lws_write(wsi, NULL, 0, LWS_WRITE_CONTINUATION) < 0)
+ return LWS_HP_RET_BAIL_DIE;
+ /* leave POLLOUT active */
+ return LWS_HP_RET_BAIL_OK;
+ }
+
+ /* Priority 6: extensions
+ */
+ if (!wsi->ws->extension_data_pending)
+ return LWS_HP_RET_USER_SERVICE;
+
+ /*
+ * check in on the active extensions, see if they
+ * had pending stuff to spill... they need to get the
+ * first look-in otherwise sequence will be disordered
+ *
+ * NULL, zero-length ebuf means just spill pending
+ */
+
+ ret = 1;
+ if (wsi->role_ops == &role_ops_raw_skt ||
+ wsi->role_ops == &role_ops_raw_file)
+ ret = 0;
+
+ while (ret == 1) {
+
+ /* default to nobody has more to spill */
+
+ ret = 0;
+ ebuf.token = NULL;
+ ebuf.len = 0;
+
+ /* give every extension a chance to spill */
+
+ m = lws_ext_cb_active(wsi, LWS_EXT_CB_PACKET_TX_PRESEND,
+ &ebuf, 0);
+ if (m < 0) {
+ lwsl_err("ext reports fatal error\n");
+ return LWS_HP_RET_BAIL_DIE;
+ }
+ if (m)
+ /*
+ * at least one extension told us he has more
+ * to spill, so we will go around again after
+ */
+ ret = 1;
+
+ /* assuming they gave us something to send, send it */
+
+ if (ebuf.len) {
+ n = lws_issue_raw(wsi, (unsigned char *)ebuf.token,
+ ebuf.len);
+ if (n < 0) {
+ lwsl_info("closing from POLLOUT spill\n");
+ return LWS_HP_RET_BAIL_DIE;
+ }
+ /*
+ * Keep amount spilled small to minimize chance of this
+ */
+ if (n != ebuf.len) {
+ lwsl_err("Unable to spill ext %d vs %d\n",
+ ebuf.len, n);
+ return LWS_HP_RET_BAIL_DIE;
+ }
+ } else
+ continue;
+
+ /* no extension has more to spill */
+
+ if (!ret)
+ continue;
+
+ /*
+ * There's more to spill from an extension, but we just sent
+ * something... did that leave the pipe choked?
+ */
+
+ if (!lws_send_pipe_choked(wsi))
+ /* no we could add more */
+ continue;
+
+ lwsl_info("choked in POLLOUT service\n");
+
+ /*
+ * Yes, he's choked. Leave the POLLOUT masked on so we will
+ * come back here when he is unchoked. Don't call the user
+ * callback to enforce ordering of spilling, he'll get called
+ * when we come back here and there's nothing more to spill.
+ */
+
+ return LWS_HP_RET_BAIL_OK;
+ }
+
+ wsi->ws->extension_data_pending = 0;
+#endif
+
+ return LWS_HP_RET_USER_SERVICE;
+}
+
+static int
+rops_periodic_checks_ws(struct lws_context *context, int tsi, time_t now)
+{
+ struct lws_vhost *vh;
+
+ if (!context->ws_ping_pong_interval ||
+ context->last_ws_ping_pong_check_s >= now + 10)
+ return 0;
+
+ vh = context->vhost_list;
+ context->last_ws_ping_pong_check_s = now;
+
+ while (vh) {
+ int n;
+
+ lws_vhost_lock(vh);
+
+ for (n = 0; n < vh->count_protocols; n++) {
+ struct lws *wsi = vh->same_vh_protocol_list[n];
+
+ while (wsi) {
+ if (lwsi_role_ws(wsi) &&
+ !wsi->socket_is_permanently_unusable &&
+ !wsi->ws->send_check_ping &&
+ wsi->ws->time_next_ping_check &&
+ lws_compare_time_t(context, now,
+ wsi->ws->time_next_ping_check) >
+ context->ws_ping_pong_interval) {
+
+ lwsl_info("req pp on wsi %p\n",
+ wsi);
+ wsi->ws->send_check_ping = 1;
+ lws_set_timeout(wsi,
+ PENDING_TIMEOUT_WS_PONG_CHECK_SEND_PING,
+ context->timeout_secs);
+ lws_callback_on_writable(wsi);
+ wsi->ws->time_next_ping_check =
+ now;
+ }
+ wsi = wsi->same_vh_protocol_next;
+ }
+ }
+
+ lws_vhost_unlock(vh);
+ vh = vh->vhost_next;
+ }
+
+ return 0;
+}
+
+static int
+rops_service_flag_pending_ws(struct lws_context *context, int tsi)
+{
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ struct lws_context_per_thread *pt = &context->pt[tsi];
+ struct lws *wsi;
+ int forced = 0;
+
+ /* POLLIN faking (the pt lock is taken by the parent) */
+
+ /*
+ * 1) For all guys with already-available ext data to drain, if they are
+ * not flowcontrolled, fake their POLLIN status
+ */
+ wsi = pt->ws.rx_draining_ext_list;
+ while (wsi && wsi->position_in_fds_table != LWS_NO_FDS_POS) {
+ pt->fds[wsi->position_in_fds_table].revents |=
+ pt->fds[wsi->position_in_fds_table].events & LWS_POLLIN;
+ if (pt->fds[wsi->position_in_fds_table].revents & LWS_POLLIN)
+ forced = 1;
+
+ wsi = wsi->ws->rx_draining_ext_list;
+ }
+
+ return forced;
+#else
+ return 0;
+#endif
+}
+
+static int
+rops_close_via_role_protocol_ws(struct lws *wsi, enum lws_close_status reason)
+{
+ if (!wsi->ws->close_in_ping_buffer_len && /* already a reason */
+ (reason == LWS_CLOSE_STATUS_NOSTATUS ||
+ reason == LWS_CLOSE_STATUS_NOSTATUS_CONTEXT_DESTROY))
+ return 0;
+
+ lwsl_debug("%s: sending close indication...\n", __func__);
+
+ /* if no prepared close reason, use 1000 and no aux data */
+
+ if (!wsi->ws->close_in_ping_buffer_len) {
+ wsi->ws->close_in_ping_buffer_len = 2;
+ wsi->ws->ping_payload_buf[LWS_PRE] = (reason >> 8) & 0xff;
+ wsi->ws->ping_payload_buf[LWS_PRE + 1] = reason & 0xff;
+ }
+
+ wsi->waiting_to_send_close_frame = 1;
+ wsi->close_needs_ack = 1;
+ lwsi_set_state(wsi, LRS_WAITING_TO_SEND_CLOSE);
+ __lws_set_timeout(wsi, PENDING_TIMEOUT_CLOSE_SEND, 5);
+
+ lws_callback_on_writable(wsi);
+
+ return 1;
+}
+
+static int
+rops_close_role_ws(struct lws_context_per_thread *pt, struct lws *wsi)
+{
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ if (wsi->ws->rx_draining_ext) {
+ struct lws **w = &pt->ws.rx_draining_ext_list;
+
+ wsi->ws->rx_draining_ext = 0;
+ /* remove us from context draining ext list */
+ while (*w) {
+ if (*w == wsi) {
+ *w = wsi->ws->rx_draining_ext_list;
+ break;
+ }
+ w = &((*w)->ws->rx_draining_ext_list);
+ }
+ wsi->ws->rx_draining_ext_list = NULL;
+ }
+
+ if (wsi->ws->tx_draining_ext) {
+ struct lws **w = &pt->ws.tx_draining_ext_list;
+ lwsl_notice("%s: CLEARING tx_draining_ext\n", __func__);
+ wsi->ws->tx_draining_ext = 0;
+ /* remove us from context draining ext list */
+ while (*w) {
+ if (*w == wsi) {
+ *w = wsi->ws->tx_draining_ext_list;
+ break;
+ }
+ w = &((*w)->ws->tx_draining_ext_list);
+ }
+ wsi->ws->tx_draining_ext_list = NULL;
+ }
+#endif
+ lws_free_set_NULL(wsi->ws->rx_ubuf);
+
+ if (wsi->trunc_alloc)
+ /* not going to be completed... nuke it */
+ lws_free_set_NULL(wsi->trunc_alloc);
+
+ wsi->ws->ping_payload_len = 0;
+ wsi->ws->ping_pending_flag = 0;
+
+ /* deallocate any active extension contexts */
+
+ if (lws_ext_cb_active(wsi, LWS_EXT_CB_DESTROY, NULL, 0) < 0)
+ lwsl_warn("extension destruction failed\n");
+
+ return 0;
+}
+
+static int
+rops_write_role_protocol_ws(struct lws *wsi, unsigned char *buf, size_t len,
+ enum lws_write_protocol *wp)
+{
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi];
+ enum lws_write_protocol wpt;
+#endif
+ int masked7 = lwsi_role_client(wsi);
+ unsigned char is_masked_bit = 0;
+ unsigned char *dropmask = NULL;
+ struct lws_tokens ebuf;
+ size_t orig_len = len;
+ int pre = 0, n = 0;
+
+ // lwsl_err("%s: wp 0x%x len %d\n", __func__, *wp, (int)len);
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ if (wsi->ws->tx_draining_ext) {
+ /* remove us from the list */
+ struct lws **w = &pt->ws.tx_draining_ext_list;
+
+ lwsl_notice("%s: CLEARING tx_draining_ext\n", __func__);
+ wsi->ws->tx_draining_ext = 0;
+ /* remove us from context draining ext list */
+ while (*w) {
+ if (*w == wsi) {
+ *w = wsi->ws->tx_draining_ext_list;
+ break;
+ }
+ w = &((*w)->ws->tx_draining_ext_list);
+ }
+ wsi->ws->tx_draining_ext_list = NULL;
+
+ wpt = *wp;
+ *wp = (wsi->ws->tx_draining_stashed_wp & 0xc0)|
+ LWS_WRITE_CONTINUATION;
+
+ /*
+ * When we are just flushing (len == 0), we can trust the
+ * stashed wp info completely. Otherwise adjust it to the
+ * FIN status of the incoming packet.
+ */
+
+ if (!(wpt & LWS_WRITE_NO_FIN) && len)
+ *wp &= ~LWS_WRITE_NO_FIN;
+
+ lwsl_notice("FORCED draining wp to 0x%02X (stashed 0x%02X, incoming 0x%02X)\n", *wp,
+ wsi->ws->tx_draining_stashed_wp, wpt);
+ // assert(0);
+ }
+#endif
+ lws_restart_ws_ping_pong_timer(wsi);
+
+ if (((*wp) & 0x1f) == LWS_WRITE_HTTP ||
+ ((*wp) & 0x1f) == LWS_WRITE_HTTP_FINAL ||
+ ((*wp) & 0x1f) == LWS_WRITE_HTTP_HEADERS_CONTINUATION ||
+ ((*wp) & 0x1f) == LWS_WRITE_HTTP_HEADERS)
+ goto send_raw;
+
+
+
+ /* if we are continuing a frame that already had its header done */
+
+ if (wsi->ws->inside_frame) {
+ lwsl_debug("INSIDE FRAME\n");
+ goto do_more_inside_frame;
+ }
+
+ wsi->ws->clean_buffer = 1;
+
+ /*
+ * give a chance to the extensions to modify payload
+ * the extension may decide to produce unlimited payload erratically
+ * (eg, compression extension), so we require only that if he produces
+ * something, it will be a complete fragment of the length known at
+ * the time (just the fragment length known), and if he has
+ * more we will come back next time he is writeable and allow him to
+ * produce more fragments until he's drained.
+ *
+ * This allows what is sent each time it is writeable to be limited to
+ * a size that can be sent without partial sends or blocking, allows
+ * interleaving of control frames and other connection service.
+ */
+ ebuf.token = (char *)buf;
+ ebuf.len = (int)len;
+
+ switch ((int)*wp) {
+ case LWS_WRITE_PING:
+ case LWS_WRITE_PONG:
+ case LWS_WRITE_CLOSE:
+ break;
+ default:
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ // lwsl_notice("LWS_EXT_CB_PAYLOAD_TX\n");
+ // m = (int)ebuf.len;
+ /* returns 0 if no more tx pending, 1 if more pending */
+ n = lws_ext_cb_active(wsi, LWS_EXT_CB_PAYLOAD_TX, &ebuf, *wp);
+ if (n < 0)
+ return -1;
+ // lwsl_notice("ext processed %d plaintext into %d compressed (wp 0x%x)\n", m, (int)ebuf.len, *wp);
+
+ if (n && ebuf.len) {
+ lwsl_notice("write drain len %d (wp 0x%x) SETTING tx_draining_ext\n", (int)ebuf.len, *wp);
+ /* extension requires further draining */
+ wsi->ws->tx_draining_ext = 1;
+ wsi->ws->tx_draining_ext_list = pt->ws.tx_draining_ext_list;
+ pt->ws.tx_draining_ext_list = wsi;
+ /* we must come back to do more */
+ lws_callback_on_writable(wsi);
+ /*
+ * keep a copy of the write type for the overall
+ * action that has provoked generation of these
+ * fragments, so the last guy can use its FIN state.
+ */
+ wsi->ws->tx_draining_stashed_wp = *wp;
+ /* this is definitely not actually the last fragment
+ * because the extension asserted he has more coming
+ * So make sure this intermediate one doesn't go out
+ * with a FIN.
+ */
+ *wp |= LWS_WRITE_NO_FIN;
+ }
+#endif
+ if (ebuf.len && wsi->ws->stashed_write_pending) {
+ wsi->ws->stashed_write_pending = 0;
+ *wp = ((*wp) & 0xc0) | (int)wsi->ws->stashed_write_type;
+ }
+ }
+
+ /*
+ * an extension did something we need to keep... for example, if
+ * compression extension, it has already updated its state according
+ * to this being issued
+ */
+ if ((char *)buf != ebuf.token) {
+ /*
+ * ext might eat it, but not have anything to issue yet.
+ * In that case we have to follow his lead, but stash and
+ * replace the write type that was lost here the first time.
+ */
+ if (len && !ebuf.len) {
+ if (!wsi->ws->stashed_write_pending)
+ wsi->ws->stashed_write_type = (char)(*wp) & 0x3f;
+ wsi->ws->stashed_write_pending = 1;
+ return (int)len;
+ }
+ /*
+ * extension recreated it:
+ * need to buffer this if not all sent
+ */
+ wsi->ws->clean_buffer = 0;
+ }
+
+ buf = (unsigned char *)ebuf.token;
+ len = ebuf.len;
+
+ if (!buf) {
+ lwsl_err("null buf (%d)\n", (int)len);
+ return -1;
+ }
+
+ switch (wsi->ws->ietf_spec_revision) {
+ case 13:
+ if (masked7) {
+ pre += 4;
+ dropmask = &buf[0 - pre];
+ is_masked_bit = 0x80;
+ }
+
+ switch ((*wp) & 0xf) {
+ case LWS_WRITE_TEXT:
+ n = LWSWSOPC_TEXT_FRAME;
+ break;
+ case LWS_WRITE_BINARY:
+ n = LWSWSOPC_BINARY_FRAME;
+ break;
+ case LWS_WRITE_CONTINUATION:
+ n = LWSWSOPC_CONTINUATION;
+ break;
+
+ case LWS_WRITE_CLOSE:
+ n = LWSWSOPC_CLOSE;
+ break;
+ case LWS_WRITE_PING:
+ n = LWSWSOPC_PING;
+ break;
+ case LWS_WRITE_PONG:
+ n = LWSWSOPC_PONG;
+ break;
+ default:
+ lwsl_warn("lws_write: unknown write opc / wp\n");
+ return -1;
+ }
+
+ if (!((*wp) & LWS_WRITE_NO_FIN))
+ n |= 1 << 7;
+
+ if (len < 126) {
+ pre += 2;
+ buf[-pre] = n;
+ buf[-pre + 1] = (unsigned char)(len | is_masked_bit);
+ } else {
+ if (len < 65536) {
+ pre += 4;
+ buf[-pre] = n;
+ buf[-pre + 1] = 126 | is_masked_bit;
+ buf[-pre + 2] = (unsigned char)(len >> 8);
+ buf[-pre + 3] = (unsigned char)len;
+ } else {
+ pre += 10;
+ buf[-pre] = n;
+ buf[-pre + 1] = 127 | is_masked_bit;
+#if defined __LP64__
+ buf[-pre + 2] = (len >> 56) & 0x7f;
+ buf[-pre + 3] = len >> 48;
+ buf[-pre + 4] = len >> 40;
+ buf[-pre + 5] = len >> 32;
+#else
+ buf[-pre + 2] = 0;
+ buf[-pre + 3] = 0;
+ buf[-pre + 4] = 0;
+ buf[-pre + 5] = 0;
+#endif
+ buf[-pre + 6] = (unsigned char)(len >> 24);
+ buf[-pre + 7] = (unsigned char)(len >> 16);
+ buf[-pre + 8] = (unsigned char)(len >> 8);
+ buf[-pre + 9] = (unsigned char)len;
+ }
+ }
+ break;
+ }
+
+do_more_inside_frame:
+
+ /*
+ * Deal with masking if we are in client -> server direction and
+ * the wp demands it
+ */
+
+ if (masked7) {
+ if (!wsi->ws->inside_frame)
+ if (lws_0405_frame_mask_generate(wsi)) {
+ lwsl_err("frame mask generation failed\n");
+ return -1;
+ }
+
+ /*
+ * in v7, just mask the payload
+ */
+ if (dropmask) { /* never set if already inside frame */
+ for (n = 4; n < (int)len + 4; n++)
+ dropmask[n] = dropmask[n] ^ wsi->ws->mask[
+ (wsi->ws->mask_idx++) & 3];
+
+ /* copy the frame nonce into place */
+ memcpy(dropmask, wsi->ws->mask, 4);
+ }
+ }
+
+ if (lwsi_role_h2_ENCAPSULATION(wsi)) {
+ struct lws *encap = lws_get_network_wsi(wsi);
+
+ assert(encap != wsi);
+ return encap->role_ops->write_role_protocol(wsi, buf - pre,
+ len + pre, wp);
+ }
+
+ switch ((*wp) & 0x1f) {
+ case LWS_WRITE_TEXT:
+ case LWS_WRITE_BINARY:
+ case LWS_WRITE_CONTINUATION:
+ if (!wsi->h2_stream_carries_ws) {
+
+ /*
+ * give any active extensions a chance to munge the
+ * buffer before send. We pass in a pointer to an
+ * lws_tokens struct prepared with the default buffer
+ * and content length that's in there. Rather than
+ * rewrite the default buffer, extensions that expect
+ * to grow the buffer can adapt .token to point to their
+ * own per-connection buffer in the extension user
+ * allocation. By default with no extensions or no
+ * extension callback handling, just the normal input
+ * buffer is used then so it is efficient.
+ *
+ * callback returns 1 in case it wants to spill more
+ * buffers
+ *
+ * This takes care of holding the buffer if send is
+ * incomplete, ie, if wsi->ws->clean_buffer is 0
+ * (meaning an extension meddled with the buffer). If
+ * wsi->ws->clean_buffer is 1, it will instead return
+ * to the user code how much OF THE USER BUFFER was
+ * consumed.
+ */
+
+ n = lws_issue_raw_ext_access(wsi, buf - pre, len + pre);
+ wsi->ws->inside_frame = 1;
+ if (n <= 0)
+ return n;
+
+ if (n == (int)len + pre) {
+ /* everything in the buffer was handled
+ * (or rebuffered...) */
+ wsi->ws->inside_frame = 0;
+ return (int)orig_len;
+ }
+
+ /*
+ * it is how many bytes of user buffer got sent... may
+ * be < orig_len in which case callback when writable
+ * has already been arranged and user code can call
+ * lws_write() again with the rest later.
+ */
+
+ return n - pre;
+ }
+ break;
+ default:
+ break;
+ }
+
+send_raw:
+ return lws_issue_raw(wsi, (unsigned char *)buf - pre, len + pre);
+}
+
+static int
+rops_close_kill_connection_ws(struct lws *wsi, enum lws_close_status reason)
+{
+ /* deal with ws encapsulation in h2 */
+#if defined(LWS_WITH_HTTP2)
+ if (wsi->http2_substream && wsi->h2_stream_carries_ws)
+ return role_ops_h2.close_kill_connection(wsi, reason);
+
+ return 0;
+#else
+ return 0;
+#endif
+}
+
+static int
+rops_callback_on_writable_ws(struct lws *wsi)
+{
+#if defined(LWS_WITH_HTTP2)
+ if (lwsi_role_h2_ENCAPSULATION(wsi)) {
+ /* we know then that it has an h2 parent */
+ struct lws *enc = role_ops_h2.encapsulation_parent(wsi);
+
+ assert(enc);
+ if (enc->role_ops->callback_on_writable(wsi))
+ return 1;
+ }
+#endif
+ return 0;
+}
+
+static int
+rops_init_vhost_ws(struct lws_vhost *vh,
+ const struct lws_context_creation_info *info)
+{
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+#ifdef LWS_WITH_PLUGINS
+ struct lws_plugin *plugin = vh->context->plugin_list;
+ int m;
+
+ if (vh->context->plugin_extension_count) {
+
+ m = 0;
+ while (info->extensions && info->extensions[m].callback)
+ m++;
+
+ /*
+ * give the vhost a unified list of extensions including the
+ * ones that came from plugins
+ */
+ vh->ws.extensions = lws_zalloc(sizeof(struct lws_extension) *
+ (m + vh->context->plugin_extension_count + 1),
+ "extensions");
+ if (!vh->ws.extensions)
+ return 1;
+
+ memcpy((struct lws_extension *)vh->ws.extensions, info->extensions,
+ sizeof(struct lws_extension) * m);
+ plugin = vh->context->plugin_list;
+ while (plugin) {
+ memcpy((struct lws_extension *)&vh->ws.extensions[m],
+ plugin->caps.extensions,
+ sizeof(struct lws_extension) *
+ plugin->caps.count_extensions);
+ m += plugin->caps.count_extensions;
+ plugin = plugin->list;
+ }
+ } else
+#endif
+ vh->ws.extensions = info->extensions;
+#endif
+
+ return 0;
+}
+
+static int
+rops_destroy_vhost_ws(struct lws_vhost *vh)
+{
+#ifdef LWS_WITH_PLUGINS
+#if !defined(LWS_WITHOUT_EXTENSIONS)
+ if (vh->context->plugin_extension_count)
+ lws_free((void *)vh->ws.extensions);
+#endif
+#endif
+
+ return 0;
+}
+
+static int
+rops_destroy_role_ws(struct lws *wsi)
+{
+ lws_free_set_NULL(wsi->ws);
+
+ return 0;
+}
+
+struct lws_role_ops role_ops_ws = {
+ /* role name */ "ws",
+ /* alpn id */ NULL,
+ /* check_upgrades */ NULL,
+ /* init_context */ NULL,
+ /* init_vhost */ rops_init_vhost_ws,
+ /* destroy_vhost */ rops_destroy_vhost_ws,
+ /* periodic_checks */ rops_periodic_checks_ws,
+ /* service_flag_pending */ rops_service_flag_pending_ws,
+ /* handle_POLLIN */ rops_handle_POLLIN_ws,
+ /* handle_POLLOUT */ rops_handle_POLLOUT_ws,
+ /* perform_user_POLLOUT */ NULL,
+ /* callback_on_writable */ rops_callback_on_writable_ws,
+ /* tx_credit */ NULL,
+ /* write_role_protocol */ rops_write_role_protocol_ws,
+ /* encapsulation_parent */ NULL,
+ /* alpn_negotiated */ NULL,
+ /* close_via_role_protocol */ rops_close_via_role_protocol_ws,
+ /* close_role */ rops_close_role_ws,
+ /* close_kill_connection */ rops_close_kill_connection_ws,
+ /* destroy_role */ rops_destroy_role_ws,
+ /* writeable cb clnt, srv */ { LWS_CALLBACK_CLIENT_WRITEABLE,
+ LWS_CALLBACK_SERVER_WRITEABLE },
+ /* close cb clnt, srv */ { LWS_CALLBACK_CLIENT_CLOSED,
+ LWS_CALLBACK_CLOSED },
+ /* file handles */ 0
+};