/* Websocket support for esphttpd. Inspired by https://github.com/dangrie158/ESP-8266-WebSocket */ /* * ---------------------------------------------------------------------------- * "THE BEER-WARE LICENSE" (Revision 42): * Jeroen Domburg wrote this file. As long as you retain * this notice you can do whatever you want with this stuff. If we meet some day, * and you think this stuff is worth it, you can buy me a beer in return. * ---------------------------------------------------------------------------- */ #include #include "httpd.h" #include "httpd-platform.h" #include "utils/sha1.h" #include "utils/base64.h" #include "cgi-websocket.h" #include "httpd-logging.h" #include "httpd-heap.h" #define WS_KEY_IDENTIFIER "Sec-WebSocket-Key: " #define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" /* from IEEE RFC6455 sec 5.2 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|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+ */ #define FLAG_FIN (1 << 7) #define OPCODE_CONTINUE 0x0 #define OPCODE_TEXT 0x1 #define OPCODE_BINARY 0x2 #define OPCODE_CLOSE 0x8 #define OPCODE_PING 0x9 #define OPCODE_PONG 0xA #define FLAGS_MASK ((uint8_t) 0xF0) #define OPCODE_MASK ((uint8_t) 0x0F) #define IS_MASKED ((uint8_t) 0x80) #define PAYLOAD_MASK ((uint8_t) 0x7F) typedef struct WebsockFrame WebsockFrame; #define ST_FLAGS 0 #define ST_LEN0 1 #define ST_LEN1 2 #define ST_LEN2 3 //... #define ST_LEN8 9 #define ST_MASK1 10 #define ST_MASK4 13 #define ST_PAYLOAD 14 /// Parsed last frame struct WebsockFrame { uint8_t flags; /// Short len; 127 or 126 indicates the longer len field is used. /// Bit 7 is the IS_MASKED flag uint8_t len8; uint64_t len; uint8_t mask[4]; }; struct WebsockPriv { /// Parsed last frame - incomplete before the parsing finishes, which can span multiple tcp frames struct WebsockFrame fr; /// Mask counter - used to cycle through mask bytes uint8_t maskCtr; /// True if the current frame isn't fully received yet and more data will come uint8_t frameCont; /// True if we initiated the close handshake - this prevents a close frame loop uint8_t closedHere; /// One of the ST_* constants int wsStatus; Websock *next; //in linked list }; static Websock *llStart = NULL; static int sendFrameHead(Websock *ws, uint8_t opcode, size_t len) { uint8_t buf[14]; size_t i = 0; buf[i++] = opcode; if (len > 65535) { buf[i++] = 127; buf[i++] = 0; buf[i++] = 0; buf[i++] = 0; buf[i++] = 0; buf[i++] = (uint8_t) (len >> 24); buf[i++] = (uint8_t) (len >> 16); buf[i++] = (uint8_t) (len >> 8); buf[i++] = (uint8_t) (len); } else if (len > 125) { buf[i++] = 126; buf[i++] = (uint8_t) (len >> 8); buf[i++] = (uint8_t) (len & 0xFF); } else { buf[i++] = (uint8_t) len; } // ws_dbg("WS: Sent frame head for payload of %d bytes.", len); return httpdSend(ws->conn, buf, i); } int cgiWebsocketSend(Websock *ws, const uint8_t *data, size_t len, int flags) { int r = 0; uint8_t fl = 0; // Continuation frame has opcode 0 if (!(flags & WEBSOCK_FLAG_CONT)) { if (flags & WEBSOCK_FLAG_BIN) { fl = OPCODE_BINARY; } else { fl = OPCODE_TEXT; } } // add FIN to last frame if (!(flags & WEBSOCK_FLAG_MORE)) { fl |= FLAG_FIN; } sendFrameHead(ws, fl, len); if (len != 0) { r = httpdSend(ws->conn, data, len); } r &= httpdFlushSendBuffer(ws->conn); return r; } //Broadcast data to all websockets at a specific url. Returns the amount of connections sent to. int cgiWebsockBroadcast(const char *resource, const uint8_t *data, size_t len, int flags) { Websock *lw = llStart; int ret = 0; while (lw != NULL) { if (strcmp(lw->conn->url, resource) == 0) { httpdConnSendStart(lw->conn); if (!cgiWebsocketSend(lw, data, len, flags)) { // send failed, do not try further (assume memory is clogged due to max backlog size) // HACK: If conn->conn is not NULL, {httpdConnSendFinish} would try to flush again // (cgiWebsocketSend already tried to flush, and that's where the error came from, possibly) // we remove it temporarily to bypass that ConnTypePtr oldpriv = lw->conn->conn; lw->conn->conn = NULL; httpdConnSendFinish(lw->conn); // -> and now we put it back for later lw->conn->conn = oldpriv; // Abort return -1; } httpdConnSendFinish(lw->conn); ret++; } lw = lw->priv->next; } return ret; } /** this is used for estimation how full the ram is */ void cgiWebsockMeasureBacklog(const char *resource, size_t *total, size_t *max) { Websock *lw = llStart; size_t bMax = 0; size_t bTotal = 0; while (lw != NULL) { if (strcmp(lw->conn->url, resource) == 0) { //lw->conn size_t bs = httpGetBacklogSize(lw->conn); bTotal += bs; if (bs > bMax) { bMax = bs; } } lw = lw->priv->next; } *total = bTotal; *max = bMax; } void cgiWebsocketClose(Websock *ws, websock_close_reason reason) { uint8_t rs[2] = {(uint8_t) (reason >> 8), reason & 0xff}; sendFrameHead(ws, FLAG_FIN | OPCODE_CLOSE, 2); httpdSend(ws->conn, rs, 2); ws->priv->closedHere = 1; httpdFlushSendBuffer(ws->conn); } static void websockFree(Websock *ws) { ws_dbg("Ws: Free"); if (ws->closeCb) { ws->closeCb(ws); } //Clean up linked list if (llStart == ws) { llStart = ws->priv->next; } else if (llStart) { Websock *lws = llStart; //Find ws that links to this one. while (lws != NULL && lws->priv->next != ws) { lws = lws->priv->next; } if (lws != NULL) { lws->priv->next = ws->priv->next; } } if (ws->priv) { httpdFree(ws->priv); } } httpd_cgi_state cgiWebSocketRecv(HttpdConnData *connData, uint8_t *data, size_t len) { size_t j, sl; httpd_cgi_state r = HTTPD_CGI_MORE; int wasHeaderByte; Websock *ws = (Websock *) connData->cgiData; for (size_t i = 0; i < len; i++) { // httpd_printf("Ws: State %d byte 0x%02X\n", ws->priv->wsStatus, data[i]); wasHeaderByte = 1; if (ws->priv->wsStatus == ST_FLAGS) { ws->priv->maskCtr = 0; ws->priv->frameCont = 0; ws->priv->fr.flags = (uint8_t) data[i]; ws->priv->wsStatus = ST_LEN0; } else if (ws->priv->wsStatus == ST_LEN0) { ws->priv->fr.len8 = (uint8_t) data[i]; if ((ws->priv->fr.len8 & 127) >= 126) { ws->priv->fr.len = 0; ws->priv->wsStatus = ST_LEN1; } else { ws->priv->fr.len = ws->priv->fr.len8 & 127; ws->priv->wsStatus = (ws->priv->fr.len8 & IS_MASKED) ? ST_MASK1 : ST_PAYLOAD; } } else if (ws->priv->wsStatus <= ST_LEN8) { ws->priv->fr.len = (ws->priv->fr.len << 8) | data[i]; if (((ws->priv->fr.len8 & 127) == 126 && ws->priv->wsStatus == ST_LEN2) || ws->priv->wsStatus == ST_LEN8) { ws->priv->wsStatus = (ws->priv->fr.len8 & IS_MASKED) ? ST_MASK1 : ST_PAYLOAD; } else { ws->priv->wsStatus++; } } else if (ws->priv->wsStatus <= ST_MASK4) { ws->priv->fr.mask[ws->priv->wsStatus - ST_MASK1] = data[i]; ws->priv->wsStatus++; } else { //Was a payload byte. wasHeaderByte = 0; } if (ws->priv->wsStatus == ST_PAYLOAD && wasHeaderByte) { //We finished parsing the header, but i still is on the last header byte. Move one forward so //the payload code works as usual. i++; } //Also finish parsing frame if we haven't received any payload bytes yet, but the length of the frame //is zero. if (ws->priv->wsStatus == ST_PAYLOAD) { //Okay, header is in; this is a data byte. We're going to process all the data bytes we have //received here at the same time; no more byte iterations till the end of this frame. //First, unmask the data sl = len - i; // ws_dbg("Ws: Frame payload. wasHeaderByte %d fr.len %d sl %d cmd 0x%x", wasHeaderByte, (int)ws->priv->fr.len, (int)sl, ws->priv->fr.flags); if ((uint64_t) sl > ws->priv->fr.len) { sl = ws->priv->fr.len; } for (j = 0; j < sl; j++) { data[i + j] ^= (ws->priv->fr.mask[(ws->priv->maskCtr++) & 3]); } // if (DEBUG_WS) { // ws_dbg("Unmasked: "); // for (j = 0; j < sl; j++) httpd_printf("%02X ", data[i + j] & 0xff); // ws_dbg("\n"); // } //Inspect the header to see what we need to do. if ((ws->priv->fr.flags & OPCODE_MASK) == OPCODE_PING) { if (ws->priv->fr.len > 125) { if (!ws->priv->frameCont) { cgiWebsocketClose(ws, WS_CLOSE_PROTOCOL_ERR); } r = HTTPD_CGI_DONE; break; } else { if (!ws->priv->frameCont) { sendFrameHead(ws, OPCODE_PONG | FLAG_FIN, ws->priv->fr.len); } if (sl > 0) { httpdSend(ws->conn, data + i, sl); } } } else if ((ws->priv->fr.flags & OPCODE_MASK) == OPCODE_TEXT || (ws->priv->fr.flags & OPCODE_MASK) == OPCODE_BINARY || (ws->priv->fr.flags & OPCODE_MASK) == OPCODE_CONTINUE) { if ((uint64_t) sl > ws->priv->fr.len) { sl = ws->priv->fr.len; } if (!(ws->priv->fr.len8 & IS_MASKED)) { //We're a server; client should send us masked packets. cgiWebsocketClose(ws, WS_CLOSE_PROTOCOL_ERR); r = HTTPD_CGI_DONE; break; } else { int flags = 0; if ((ws->priv->fr.flags & OPCODE_MASK) == OPCODE_BINARY) { flags |= WEBSOCK_FLAG_BIN; } if ((ws->priv->fr.flags & FLAG_FIN) == 0) { flags |= WEBSOCK_FLAG_MORE; } if (ws->recvCb) { ws->recvCb(ws, data + i, sl, flags); } } } else if ((ws->priv->fr.flags & OPCODE_MASK) == OPCODE_CLOSE) { // ws_dbg("WS: Got close frame"); if (!ws->priv->closedHere) { // ws_dbg("WS: Sending response close frame, %x %x (i=%d, len=%d)", data[i], data[i+1], i, len); websock_close_reason cause = WS_CLOSE_OK; if (i <= len - 2) { // TODO why? cause = ((data[i] << 8) & 0xff00) + (data[i + 1] & 0xff); } cgiWebsocketClose(ws, cause); } r = HTTPD_CGI_DONE; break; } else { if (!ws->priv->frameCont) { ws_error("WS: Unknown opcode 0x%X", ws->priv->fr.flags & OPCODE_MASK); } } i += (size_t) (sl - 1); ws->priv->fr.len -= (uint64_t) sl; if (ws->priv->fr.len == 0) { ws->priv->wsStatus = ST_FLAGS; //go receive next frame } else { ws->priv->frameCont = 1; //next payload is continuation of this frame. } } } if (r == HTTPD_CGI_DONE) { //We're going to tell the main webserver we're done. The webserver expects us to clean up by ourselves //we're chosing to be done. Do so. websockFree(ws); httpdFree(connData->cgiData); connData->cgiData = NULL; } return r; } //Websocket 'cgi' implementation httpd_cgi_state cgiWebsocket(HttpdConnData *connData) { char buff[256]; int i; httpd_sha1nfo s; if (connData->conn == NULL) { //Connection aborted. Clean up. ws_dbg("WS: Cleanup"); if (connData->cgiData) { Websock *ws = (Websock *) connData->cgiData; websockFree(ws); httpdFree(connData->cgiData); connData->cgiData = NULL; } return HTTPD_CGI_DONE; } if (connData->cgiData == NULL) { // httpd_printf("WS: First call\n"); //First call here. Check if client headers are OK, send server header. i = httpdGetHeader(connData, "Upgrade", buff, sizeof(buff) - 1); ws_dbg("WS: Upgrade: %s", buff); if (i && strcasecmp(buff, "websocket") == 0) { i = httpdGetHeader(connData, "Sec-WebSocket-Key", buff, sizeof(buff) - 1); if (i) { WsConnectedCb connCb = connData->cgiArg; if (!connCb) { ws_error("WS route missing connCb!"); return HTTPD_CGI_DONE; } // httpd_printf("WS: Key: %s\n", buff); //Seems like a WebSocket connection. // Alloc structs connData->cgiData = httpdMalloc(sizeof(Websock)); if (connData->cgiData == NULL) { ws_error("Can't allocate mem for websocket"); return HTTPD_CGI_DONE; } memset(connData->cgiData, 0, sizeof(Websock)); Websock *ws = (Websock *) connData->cgiData; ws->priv = httpdMalloc(sizeof(WebsockPriv)); if (ws->priv == NULL) { ws_error("Can't allocate mem for websocket priv"); httpdFree(connData->cgiData); connData->cgiData = NULL; return HTTPD_CGI_DONE; } memset(ws->priv, 0, sizeof(WebsockPriv)); ws->conn = connData; //Reply with the right headers. strcat(buff, WS_GUID); httpd_sha1_init(&s); httpd_sha1_write(&s, buff, strlen(buff)); httdSetTransferMode(connData, HTTPD_TRANSFER_NONE); httpdStartResponse(connData, 101); httpdHeader(connData, "Upgrade", "websocket"); httpdHeader(connData, "Connection", "upgrade"); httpd_base64_encode(20, httpd_sha1_result(&s), sizeof(buff), buff); httpdHeader(connData, "Sec-WebSocket-Accept", buff); httpdEndHeaders(connData); //Set data receive handler connData->recvHdl = cgiWebSocketRecv; //Inform CGI function we have a connection connCb(ws); //Insert ws into linked list if (llStart == NULL) { llStart = ws; } else { Websock *lw = llStart; while (lw->priv->next) { lw = lw->priv->next; } lw->priv->next = ws; } return HTTPD_CGI_MORE; } } //No valid websocket connection httpdStartResponse(connData, 500); httpdEndHeaders(connData); return HTTPD_CGI_DONE; } //Sending is done. Call the sent callback if we have one. Websock *ws = (Websock *) connData->cgiData; if (ws && ws->sentCb) { ws->sentCb(ws); } return HTTPD_CGI_MORE; }