You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
484 lines
17 KiB
484 lines
17 KiB
/*
|
|
Websocket support for esphttpd. Inspired by https://github.com/dangrie158/ESP-8266-WebSocket
|
|
*/
|
|
|
|
/*
|
|
* ----------------------------------------------------------------------------
|
|
* "THE BEER-WARE LICENSE" (Revision 42):
|
|
* Jeroen Domburg <jeroen@spritesmods.com> 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 <string.h>
|
|
#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;
|
|
}
|
|
|