/*! \brief Maximum reconstruction size for multi-frame payload reconstruction. */
#define MAXIMUM_RECONSTRUCTION_CEILING 16384
+/*! \brief Maximum size of a websocket frame header
+ * 1 byte flags and opcode
+ * 1 byte mask flag + payload len
+ * 8 bytes max extended length
+ * 4 bytes optional masking key
+ * ... payload follows ...
+ * */
+#define MAX_WS_HDR_SZ 14
+#define MIN_WS_HDR_SZ 2
+
/*! \brief Structure definition for session */
struct ast_websocket {
FILE *f; /*!< Pointer to the file instance used for writing and reading */
if (fwrite(payload, 1, actual_length, session->f) != actual_length) {
return -1;
}
+ fflush(session->f);
return 0;
}
return 0;
}
+/* MAINTENANCE WARNING on ast_websocket_read()!
+ *
+ * We have to keep in mind during this function that the fact that session->fd seems ready
+ * (via poll) does not necessarily mean we have application data ready, because in the case
+ * of an SSL socket, there is some encryption data overhead that needs to be read from the
+ * TCP socket, so poll() may say there are bytes to be read, but whether it is just 1 byte
+ * or N bytes we do not know that, and we do not know how many of those bytes (if any) are
+ * for application data (for us) and not just for the SSL protocol consumption
+ *
+ * There used to be a couple of nasty bugs here that were fixed in last refactoring but I
+ * want to document them so the constraints are clear and we do not re-introduce them:
+ *
+ * - This function would incorrectly assume that fread() would necessarily return more than
+ * 1 byte of data, just because a websocket frame is always >= 2 bytes, but the thing
+ * is we're dealing with a TCP bitstream here, we could read just one byte and that's normal.
+ * The problem before was that if just one byte was read, the function bailed out and returned
+ * an error, effectively dropping the first byte of a websocket frame header!
+ *
+ * - Another subtle bug was that it would just read up to MAX_WS_HDR_SZ (14 bytes) via fread()
+ * then assume that executing poll() would tell you if there is more to read, but since
+ * we're dealing with a buffered stream (session->f is a FILE*), poll would say there is
+ * nothing else to read (in the real tcp socket session->fd) and we would get stuck here
+ * without processing the rest of the data in session->f internal buffers until another packet
+ * came on the network to unblock us!
+ *
+ * Note during the header parsing stage we try to read in small chunks just what we need, this
+ * is buffered data anyways, no expensive syscall required most of the time ...
+ */
+static inline int ws_safe_read(struct ast_websocket *session, char *buf, int len, enum ast_websocket_opcode *opcode)
+{
+ int sanity;
+ size_t rlen;
+ int xlen = len;
+ char *rbuf = buf;
+ for (sanity = 10; sanity; sanity--) {
+ clearerr(session->f);
+ rlen = fread(rbuf, 1, xlen, session->f);
+ if (0 == rlen && ferror(session->f) && errno != EAGAIN) {
+ ast_log(LOG_ERROR, "Error reading from web socket: %s\n", strerror(errno));
+ (*opcode) = AST_WEBSOCKET_OPCODE_CLOSE;
+ session->closing = 1;
+ return -1;
+ }
+ xlen = (xlen - rlen);
+ rbuf = rbuf + rlen;
+ if (0 == xlen) {
+ break;
+ }
+ if (ast_wait_for_input(session->fd, 1000) < 0) {
+ ast_log(LOG_ERROR, "ast_wait_for_input returned err: %s\n", strerror(errno));
+ (*opcode) = AST_WEBSOCKET_OPCODE_CLOSE;
+ session->closing = 1;
+ return -1;
+ }
+ }
+ if (!sanity) {
+ ast_log(LOG_WARNING, "Websocket seems unresponsive, disconnecting ...\n");
+ (*opcode) = AST_WEBSOCKET_OPCODE_CLOSE;
+ session->closing = 1;
+ return -1;
+ }
+ return 0;
+}
+
int AST_OPTIONAL_API_NAME(ast_websocket_read)(struct ast_websocket *session, char **payload, uint64_t *payload_len, enum ast_websocket_opcode *opcode, int *fragmented)
{
char buf[MAXIMUM_FRAME_SIZE] = "";
- size_t frame_size, expected = 2;
+ int fin = 0;
+ int mask_present = 0;
+ char *mask = NULL, *new_payload = NULL;
+ size_t options_len = 0, frame_size = 0;
*payload = NULL;
*payload_len = 0;
*fragmented = 0;
- /* We try to read in 14 bytes, which is the largest possible WebSocket header */
- if ((frame_size = fread(&buf, 1, 14, session->f)) < 1) {
- return -1;
- }
-
- /* The minimum size for a WebSocket frame is 2 bytes */
- if (frame_size < expected) {
- return -1;
+ if (ws_safe_read(session, &buf[0], MIN_WS_HDR_SZ, opcode)) {
+ return 0;
}
+ frame_size += MIN_WS_HDR_SZ;
+ /* ok, now we have the first 2 bytes, so we know some flags, opcode and payload length (or whether payload length extension will be required) */
*opcode = buf[0] & 0xf;
-
+ *payload_len = buf[1] & 0x7f;
if (*opcode == AST_WEBSOCKET_OPCODE_TEXT || *opcode == AST_WEBSOCKET_OPCODE_BINARY || *opcode == AST_WEBSOCKET_OPCODE_CONTINUATION ||
*opcode == AST_WEBSOCKET_OPCODE_PING || *opcode == AST_WEBSOCKET_OPCODE_PONG) {
- int fin = (buf[0] >> 7) & 1;
- int mask_present = (buf[1] >> 7) & 1;
- char *mask = NULL, *new_payload;
- size_t remaining;
-
- if (mask_present) {
- /* The mask should take up 4 bytes */
- expected += 4;
-
- if (frame_size < expected) {
- /* Per the RFC 1009 means we received a message that was too large for us to process */
- ast_websocket_close(session, 1009);
+ fin = (buf[0] >> 7) & 1;
+ mask_present = (buf[1] >> 7) & 1;
+
+ /* Based on the mask flag and payload length, determine how much more we need to read before start parsing the rest of the header */
+ options_len += mask_present ? 4 : 0;
+ options_len += (*payload_len == 126) ? 2 : (*payload_len == 127) ? 8 : 0;
+ if (options_len) {
+ /* read the rest of the header options */
+ if (ws_safe_read(session, &buf[frame_size], options_len, opcode)) {
return 0;
}
+ frame_size += options_len;
}
- /* Assume no extended length and no masking at the beginning */
- *payload_len = buf[1] & 0x7f;
- *payload = &buf[2];
-
- /* Determine if extended length is being used */
if (*payload_len == 126) {
- /* Use the next 2 bytes to get a uint16_t */
- expected += 2;
- *payload += 2;
-
- if (frame_size < expected) {
- ast_websocket_close(session, 1009);
- return 0;
- }
-
+ /* Grab the 2-byte payload length */
*payload_len = ntohs(get_unaligned_uint16(&buf[2]));
+ mask = &buf[4];
} else if (*payload_len == 127) {
- /* Use the next 8 bytes to get a uint64_t */
- expected += 8;
- *payload += 8;
-
- if (frame_size < expected) {
- ast_websocket_close(session, 1009);
- return 0;
- }
-
+ /* Grab the 8-byte payload length */
*payload_len = ntohl(get_unaligned_uint64(&buf[2]));
+ mask = &buf[10];
+ } else {
+ /* Just set the mask after the small 2-byte header */
+ mask = &buf[2];
}
- /* If masking is present the payload currently points to the mask, so move it over 4 bytes to the actual payload */
- if (mask_present) {
- mask = *payload;
- *payload += 4;
- }
-
- /* Determine how much payload we need to read in as we may have already read some in */
- remaining = *payload_len - (frame_size - expected);
-
- /* If how much payload they want us to read in exceeds what we are capable of close the session, things
- * will fail no matter what most likely */
- if (remaining > (MAXIMUM_FRAME_SIZE - frame_size)) {
+ /* Now read the rest of the payload */
+ *payload = &buf[frame_size]; /* payload will start here, at the end of the options, if any */
+ frame_size = frame_size + (*payload_len); /* final frame size is header + optional headers + payload data */
+ if (frame_size > MAXIMUM_FRAME_SIZE) {
+ ast_log(LOG_WARNING, "Cannot fit huge websocket frame of %zd bytes\n", frame_size);
+ /* The frame won't fit :-( */
ast_websocket_close(session, 1009);
- return 0;
+ return -1;
}
- new_payload = *payload + (frame_size - expected);
-
- /* Read in the remaining payload */
- while (remaining > 0) {
- size_t payload_read;
-
- /* Wait for data to come in */
- if (ast_wait_for_input(session->fd, -1) <= 0) {
- *opcode = AST_WEBSOCKET_OPCODE_CLOSE;
- *payload = NULL;
- session->closing = 1;
- return 0;
- }
-
- /* If some sort of failure occurs notify the caller */
- if ((payload_read = fread(new_payload, 1, remaining, session->f)) < 1) {
- return -1;
- }
-
- remaining -= payload_read;
- new_payload += payload_read;
+ if (ws_safe_read(session, (*payload), (*payload_len), opcode)) {
+ return 0;
}
/* If a mask is present unmask the payload */
}
}
- if (!(new_payload = ast_realloc(session->payload, session->payload_len + *payload_len))) {
+ if (!(new_payload = ast_realloc(session->payload, (session->payload_len + *payload_len)))) {
+ ast_log(LOG_WARNING, "Failed allocation: %p, %zd, %lu\n",
+ session->payload, session->payload_len, *payload_len);
*payload_len = 0;
ast_websocket_close(session, 1009);
return 0;
}
session->payload = new_payload;
- memcpy(session->payload + session->payload_len, *payload, *payload_len);
+ memcpy((session->payload + session->payload_len), (*payload), (*payload_len));
session->payload_len += *payload_len;
if (!fin && session->reconstruct && (session->payload_len < session->reconstruct)) {
session->payload_len = 0;
}
} else if (*opcode == AST_WEBSOCKET_OPCODE_CLOSE) {
- char *new_payload;
-
- *payload_len = buf[1] & 0x7f;
-
/* Make the payload available so the user can look at the reason code if they so desire */
if ((*payload_len) && (new_payload = ast_realloc(session->payload, *payload_len))) {
+ if (ws_safe_read(session, &buf[frame_size], (*payload_len), opcode)) {
+ return 0;
+ }
session->payload = new_payload;
- memcpy(session->payload, &buf[2], *payload_len);
+ memcpy(session->payload, &buf[frame_size], *payload_len);
*payload = session->payload;
+ frame_size += (*payload_len);
}
if (!session->closing) {
session->f = NULL;
ast_verb(2, "WebSocket connection from '%s' closed\n", ast_sockaddr_stringify(&session->address));
} else {
+ ast_log(LOG_WARNING, "WebSocket unknown opcode %d\n", *opcode);
/* We received an opcode that we don't understand, the RFC states that 1003 is for a type of data that can't be accepted... opcodes
* fit that, I think. */
ast_websocket_close(session, 1003);
upgrade,
base64,
protocol);
+ fflush(ser->f);
} else {
/* Specification defined in http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75 or completely unknown */
{
int flags, res;
+ ast_debug(1, "Entering WebSocket echo loop\n");
+
if ((flags = fcntl(ast_websocket_fd(session), F_GETFL)) == -1) {
goto end;
}
if (ast_websocket_read(session, &payload, &payload_len, &opcode, &fragmented)) {
/* We err on the side of caution and terminate the session if any error occurs */
+ ast_log(LOG_WARNING, "Read failure during WebSocket echo loop\n");
break;
}
ast_websocket_write(session, opcode, payload, payload_len);
} else if (opcode == AST_WEBSOCKET_OPCODE_CLOSE) {
break;
+ } else {
+ ast_debug(1, "Ignored WebSocket opcode %d\n", opcode);
}
}
end:
+ ast_debug(1, "Exitting WebSocket echo loop\n");
ast_websocket_unref(session);
}