ESP8266 part of the f105-motor-demo project (see f105-motor-demo_stm32)
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.
 
 
 
 
 
 

354 lines
11 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 <esp8266.h>
#include "httpd.h"
#include "sha1.h"
#include "base64.h"
#include "cgiwebsocket.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)(1<<7))
#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
struct WebsockFrame {
uint8_t flags;
uint8_t len8;
uint64_t len;
uint8_t mask[4];
};
struct WebsockPriv {
struct WebsockFrame fr;
uint8_t maskCtr;
uint8 frameCont;
uint8 closedHere;
int wsStatus;
Websock *next; //in linked list
};
static Websock *llStart=NULL;
static int ICACHE_FLASH_ATTR sendFrameHead(Websock *ws, int opcode, int len) {
char buf[14];
int 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++]=len>>24;
buf[i++]=len>>16;
buf[i++]=len>>8;
buf[i++]=len;
} else if (len>125) {
buf[i++]=126;
buf[i++]=len>>8;
buf[i++]=len;
} else {
buf[i++]=len;
}
httpd_printf("WS: Sent frame head for payload of %d bytes.\n", len);
return httpdSend(ws->conn, buf, i);
}
int ICACHE_FLASH_ATTR cgiWebsocketSend(Websock *ws, char *data, int len, int flags) {
int r=0;
int fl=0;
if (flags&WEBSOCK_FLAG_BIN) fl=OPCODE_BINARY; else fl=OPCODE_TEXT;
if (!(flags&WEBSOCK_FLAG_CONT)) fl|=FLAG_FIN;
sendFrameHead(ws, fl, len);
if (len!=0) r=httpdSend(ws->conn, data, len);
httpdFlushSendBuffer(ws->conn);
return r;
}
//Broadcast data to all websockets at a specific url. Returns the amount of connections sent to.
int ICACHE_FLASH_ATTR cgiWebsockBroadcast(char *resource, char *data, int len, int flags) {
//This is majorly broken (and actually, always, it just tended to work because the circumstances
//were juuuust right). Because the socket is used outside of the httpd send/receive context, it
//will not have an associated send buffer. This means httpdSend will write to a dangling pointer!
//Disabled for now. If you really need this, open an issue on github or otherwise poke me and I'll
//see what I can do.
/*
Websock *lw=llStart;
int ret=0;
while (lw!=NULL) {
if (strcmp(lw->conn->url, resource)==0) {
cgiWebsocketSend(lw, data, len, flags);
ret++;
}
lw=lw->priv->next;
}
return ret;
*/
return 0;
}
void ICACHE_FLASH_ATTR cgiWebsocketClose(Websock *ws, int reason) {
char rs[2]={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 ICACHE_FLASH_ATTR websockFree(Websock *ws) {
httpd_printf("Ws: Free\n");
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) free(ws->priv);
}
int ICACHE_FLASH_ATTR cgiWebSocketRecv(HttpdConnData *connData, char *data, int len) {
int i, j, sl;
int r=HTTPD_CGI_MORE;
int wasHeaderByte;
Websock *ws=(Websock*)connData->cgiData;
for (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;
httpd_printf("Ws: Frame payload. wasHeaderByte %d fr.len %d sl %d cmd 0x%x\n", wasHeaderByte, (int)ws->priv->fr.len, (int)sl, ws->priv->fr.flags);
if (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]);
// httpd_printf("Unmasked: ");
// for (j=0; j<sl; j++) httpd_printf("%02X ", data[i+j]&0xff);
// httpd_printf("\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, 1002);
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 (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, 1002);
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_CONT;
if (ws->recvCb) ws->recvCb(ws, data+i, sl, flags);
}
} else if ((ws->priv->fr.flags&OPCODE_MASK)==OPCODE_CLOSE) {
httpd_printf("WS: Got close frame\n");
if (!ws->priv->closedHere) {
httpd_printf("WS: Sending response close frame\n");
cgiWebsocketClose(ws, ((data[i]<<8)&0xff00)+(data[i+1]&0xff));
}
r=HTTPD_CGI_DONE;
break;
} else {
if (!ws->priv->frameCont) httpd_printf("WS: Unknown opcode 0x%X\n", ws->priv->fr.flags&OPCODE_MASK);
}
i+=sl-1;
ws->priv->fr.len-=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);
free(connData->cgiData);
connData->cgiData=NULL;
}
return r;
}
//Websocket 'cgi' implementation
int ICACHE_FLASH_ATTR cgiWebsocket(HttpdConnData *connData) {
char buff[256];
int i;
sha1nfo s;
if (connData->conn==NULL) {
//Connection aborted. Clean up.
httpd_printf("WS: Cleanup\n");
if (connData->cgiData) {
Websock *ws=(Websock*)connData->cgiData;
websockFree(ws);
free(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);
httpd_printf("WS: Upgrade: %s\n", buff);
if (i && strcasecmp(buff, "websocket")==0) {
i=httpdGetHeader(connData, "Sec-WebSocket-Key", buff, sizeof(buff)-1);
if (i) {
// httpd_printf("WS: Key: %s\n", buff);
//Seems like a WebSocket connection.
// Alloc structs
connData->cgiData=malloc(sizeof(Websock));
memset(connData->cgiData, 0, sizeof(Websock));
Websock *ws=(Websock*)connData->cgiData;
ws->priv=malloc(sizeof(WebsockPriv));
memset(ws->priv, 0, sizeof(WebsockPriv));
ws->conn=connData;
//Reply with the right headers.
strcat(buff, WS_GUID);
sha1_init(&s);
sha1_write(&s, buff, strlen(buff));
httpdDisableTransferEncoding(connData);
httpdStartResponse(connData, 101);
httpdHeader(connData, "Upgrade", "websocket");
httpdHeader(connData, "Connection", "upgrade");
base64_encode(20, 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
WsConnectedCb connCb=connData->cgiArg;
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;
}