mirror of
https://github.com/Mauler125/r5sdk.git
synced 2025-02-09 19:15:03 +01:00
DirtySDK (EA's Dirty Sockets library) will be used for the LiveAPI implementation, and depends on: EABase, EAThread.
2052 lines
69 KiB
C
2052 lines
69 KiB
C
/*H********************************************************************************/
|
|
/*!
|
|
\File protowebsocket.c
|
|
|
|
\Description
|
|
This module implements a WebSocket client interface as documented by
|
|
RFC 6455 (http://tools.ietf.org/html/rfc6455). The current version of the
|
|
WebSockets protocol implemented here is version 13. Up to date registry
|
|
information regarding new extensions, protocols, result codes, etc. can be
|
|
found at http://www.iana.org/assignments/websocket/websocket.xml.
|
|
|
|
\Notes
|
|
While the standard supports up to a 64-bit frame size, this implementation
|
|
only supports up to 32-bit frame sizes.
|
|
|
|
\Todo
|
|
- Sec-WebSocket-Protocols support
|
|
|
|
\Copyright
|
|
Copyright (c) 2012 Electronic Arts Inc.
|
|
|
|
\Version 11/26/2012 (jbrookes) First Version
|
|
\Version 03/31/2017 (jbrookes) Added frame send/recv capability
|
|
*/
|
|
/********************************************************************************H*/
|
|
|
|
/*** Include files ****************************************************************/
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "DirtySDK/dirtysock.h"
|
|
#include "DirtySDK/dirtyvers.h"
|
|
#include "DirtySDK/dirtysock/dirtymem.h"
|
|
#include "DirtySDK/crypt/cryptrand.h"
|
|
#include "DirtySDK/crypt/cryptsha1.h"
|
|
#include "DirtySDK/proto/protohttp.h"
|
|
#include "DirtySDK/proto/protossl.h"
|
|
#include "DirtySDK/util/base64.h"
|
|
|
|
#include "DirtySDK/proto/protowebsocket.h"
|
|
|
|
/*** Defines **********************************************************************/
|
|
|
|
//! ProtoWebSocket revision number (maj.min)
|
|
#define PROTOWEBSOCKET_VERSION (0x0100) // update this for major bug fixes or protocol additions/changes
|
|
|
|
//! default ProtoWebSocket timeout
|
|
#define PROTOWEBSOCKET_TIMEOUT (5*30*1000)
|
|
|
|
//! define WebSocket protocol version
|
|
#define PROTOWEBSOCKET_PROTOCOLVERS (13)
|
|
|
|
//! default protowebsocket buffer size
|
|
#define PROTOWEBSOCKET_BUFSIZE_MIN (4*1024)
|
|
|
|
// opcode definitions
|
|
#define PROTOWEBSOCKET_OPCODE_BASE (0x0)
|
|
#define PROTOWEBSOCKET_OPCODE_CONT (PROTOWEBSOCKET_OPCODE_BASE+0)
|
|
#define PROTOWEBSOCKET_OPCODE_TEXT (PROTOWEBSOCKET_OPCODE_BASE+1)
|
|
#define PROTOWEBSOCKET_OPCODE_BINA (PROTOWEBSOCKET_OPCODE_BASE+2)
|
|
// control frame type definitions
|
|
#define PROTOWEBSOCKET_CTRL_BASE (0x8)
|
|
#define PROTOWEBSOCKET_CTRL_CLSE (PROTOWEBSOCKET_CTRL_BASE+0)
|
|
#define PROTOWEBSOCKET_CTRL_PING (PROTOWEBSOCKET_CTRL_BASE+1)
|
|
#define PROTOWEBSOCKET_CTRL_PONG (PROTOWEBSOCKET_CTRL_BASE+2)
|
|
// fragment end
|
|
#define PROTOWEBSOCKET_OPCODE_FIN (0x80)
|
|
|
|
// max WebSocket protocol send header size
|
|
#define PROTOWEBSOCKET_MAXSENDHDR (12)
|
|
|
|
//! default size for our temporary input buffer
|
|
#define PROTOWEBSOCKET_DEFAULT_INPUTBUFSIZE (256)
|
|
|
|
/*** Type Definitions *************************************************************/
|
|
|
|
// state for a websocket connection
|
|
struct ProtoWebSocketRefT
|
|
{
|
|
int32_t iMemGroup;
|
|
void *pMemGroupUserData;
|
|
|
|
ProtoSSLRefT *pProtoSSL;
|
|
|
|
enum
|
|
{
|
|
ST_DISC, //!< disconnected
|
|
ST_CONN, //!< connecting
|
|
ST_SEND, //!< sending handshake
|
|
ST_RECV, //!< receiving handshake
|
|
ST_OPEN, //!< connected
|
|
ST_FAIL //!< connection failed
|
|
} eState; //!< current state
|
|
|
|
int32_t iVersion; //!< protocol version
|
|
|
|
char *pOutBuf; //!< output buffer
|
|
int32_t iOutMax; //!< max amount of data that can be stored in the buffer
|
|
int32_t iOutLen; //!< current amount of data in buffer
|
|
int32_t iOutOff; //!< current offset within buffer
|
|
int32_t iRecvRslt;
|
|
int32_t iRecvSize; //!< used by RecvFrame to track total received size
|
|
|
|
char *pInpBuf; //!< temporary input buffer used for connection establishment
|
|
int32_t iInpMax; //!< max amount of data that can be stored in the buffer
|
|
int32_t iInpLen; //!< current amount of data in buffer
|
|
int32_t iInpOff; //!< current offset within buffer
|
|
|
|
int32_t iFrameHdrOff;
|
|
int32_t iFrameHdrLen;
|
|
int32_t iFrameLen;
|
|
int32_t iFrameType;
|
|
|
|
int32_t iCtrlDataLen; //!< length of control packet data
|
|
int32_t iCtrlDataOff; //!< offset while receiving control packet data
|
|
|
|
int32_t iVerbose;
|
|
uint32_t uTimer;
|
|
int32_t iTimeout;
|
|
int32_t iKeepAlive; //!< if >0, number of seconds before issuing a keep-alive pong
|
|
uint32_t uKeepTimer;
|
|
int32_t iSSLFail;
|
|
int32_t iCloseReason;
|
|
|
|
char *pAppendHdr; //!< append header buffer pointer
|
|
int32_t iAppendLen; //!< size of append header buffer
|
|
|
|
int32_t iPort; //!< current connection port
|
|
|
|
uint8_t bTimeout;
|
|
uint8_t bDoPong; //!< execute a 'pong' frame at earliest opportunity
|
|
uint8_t bDoClose; //!< execute a 'close' frame at earliest opportunity; disconnect
|
|
uint8_t bClosing; //!< have sent a close frame
|
|
uint8_t bRecvCtrlData; //!< currently receiving control data
|
|
uint8_t bMessageSend; //!< if true, we're sending a message (FIN bit not sent until message is complete)
|
|
uint8_t bFrameFrag; //!< if true, we're sending a fragmented frame
|
|
uint8_t bFrameFin; //!< if true this packet is a fin packet
|
|
|
|
char strHost[256]; //!< server name
|
|
char strFrameHdr[PROTOWEBSOCKET_MAXSENDHDR]; //!< cache to receive frame header into
|
|
|
|
char strRawKey[32]; //!< generated key sent to server; must hold 16 characters base64 encoded
|
|
char strEncKey[32]; //!< encoded key we expect to get back from server; must hold 16 characters base64 encoded
|
|
|
|
char strCtrlData[256]; //!< cached control packet data
|
|
};
|
|
|
|
/*** Variables ********************************************************************/
|
|
|
|
#if DIRTYCODE_LOGGING
|
|
static const char *_ProtoWebSocket_strOpcodeNames[16] =
|
|
{
|
|
"cont",
|
|
"text",
|
|
"bina",
|
|
"", "", "", "", "",
|
|
"clse",
|
|
"ping",
|
|
"pong",
|
|
"", "", "", "", ""
|
|
};
|
|
static const char *_ProtoWebSocket_strCloseReasons[PROTOWEBSOCKET_CLOSEREASON_COUNT] =
|
|
{
|
|
"normal",
|
|
"going away",
|
|
"protocol error",
|
|
"unsupported data",
|
|
"reserved",
|
|
"no status received",
|
|
"abnormal closure",
|
|
"invalid frame data",
|
|
"policy violation",
|
|
"message too big",
|
|
"mandatory extension",
|
|
"internal error",
|
|
"service restart",
|
|
"try again later",
|
|
"reserved",
|
|
"tls handshake",
|
|
};
|
|
#endif
|
|
|
|
/*** Function Prototypes **********************************************************/
|
|
|
|
static int32_t _ProtoWebSocketRecvData(ProtoWebSocketRefT *pWebSocket, char *pStrBuf, int32_t iSize);
|
|
|
|
/*** Private Functions ************************************************************/
|
|
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketSetAppendHeader
|
|
|
|
\Description
|
|
Set given string as append header, allocating memory as required.
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pAppendHdr - append header string
|
|
|
|
\Output
|
|
int32_t - zero=success, else error
|
|
|
|
\Version 12/04/2012 (jbrookes) Copied from ProtoHttp
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketSetAppendHeader(ProtoWebSocketRefT *pWebSocket, const char *pAppendHdr)
|
|
{
|
|
int32_t iAppendBufLen, iAppendStrLen;
|
|
|
|
// check for empty append string, in which case we free the buffer
|
|
if ((pAppendHdr == NULL) || (*pAppendHdr == '\0'))
|
|
{
|
|
if (pWebSocket->pAppendHdr != NULL)
|
|
{
|
|
DirtyMemFree(pWebSocket->pAppendHdr, PROTOHTTP_MEMID, pWebSocket->iMemGroup, pWebSocket->pMemGroupUserData);
|
|
pWebSocket->pAppendHdr = NULL;
|
|
}
|
|
pWebSocket->iAppendLen = 0;
|
|
return(0);
|
|
}
|
|
|
|
// check to see if append header is already set
|
|
if ((pWebSocket->pAppendHdr != NULL) && (!strcmp(pAppendHdr, pWebSocket->pAppendHdr)))
|
|
{
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 1, "protowebsocket: [%p] ignoring set of append header '%s' that is already set\n", pWebSocket, pAppendHdr));
|
|
return(0);
|
|
}
|
|
|
|
// get append header length
|
|
iAppendStrLen = (int32_t)strlen(pAppendHdr);
|
|
// append buffer size includes null and space for \r\n if not included by submitter
|
|
iAppendBufLen = iAppendStrLen + 3;
|
|
|
|
// see if we need to allocate a new buffer
|
|
if (iAppendBufLen > pWebSocket->iAppendLen)
|
|
{
|
|
if (pWebSocket->pAppendHdr != NULL)
|
|
{
|
|
DirtyMemFree(pWebSocket->pAppendHdr, PROTOHTTP_MEMID, pWebSocket->iMemGroup, pWebSocket->pMemGroupUserData);
|
|
}
|
|
if ((pWebSocket->pAppendHdr = DirtyMemAlloc(iAppendBufLen, PROTOHTTP_MEMID, pWebSocket->iMemGroup, pWebSocket->pMemGroupUserData)) != NULL)
|
|
{
|
|
pWebSocket->iAppendLen = iAppendBufLen;
|
|
}
|
|
else
|
|
{
|
|
NetPrintf(("protowebsocket: [%p] could not allocate %d byte buffer for append header\n", pWebSocket, iAppendBufLen));
|
|
pWebSocket->iAppendLen = 0;
|
|
return(-1);
|
|
}
|
|
}
|
|
|
|
// copy append header
|
|
ds_strnzcpy(pWebSocket->pAppendHdr, pAppendHdr, iAppendStrLen+1);
|
|
|
|
// if append header is not \r\n terminated, do it here
|
|
if (pAppendHdr[iAppendStrLen-2] != '\r' || pAppendHdr[iAppendStrLen-1] != '\n')
|
|
{
|
|
ds_strnzcat(pWebSocket->pAppendHdr, "\r\n", pWebSocket->iAppendLen);
|
|
}
|
|
return(0);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketGenerateKey
|
|
|
|
\Description
|
|
Generate random base64-encoded key to be sent to server in initial
|
|
handshake, and base64-encoded SHA1 hash of the key that we expect
|
|
from the server in response.
|
|
|
|
\Input *pWebSocket - module state
|
|
|
|
\Version 11/27/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static void _ProtoWebSocketGenerateKey(ProtoWebSocketRefT *pWebSocket)
|
|
{
|
|
char strAccept[128];
|
|
uint8_t aSha1Data[CRYPTSHA1_HASHSIZE];
|
|
CryptSha1T Sha1State;
|
|
uint8_t aRandom[16];
|
|
|
|
// get 16-digit random number
|
|
CryptRandGet(aRandom, sizeof(aRandom));
|
|
|
|
// base64 encode string
|
|
Base64Encode2((char *)aRandom, sizeof(aRandom), pWebSocket->strRawKey, sizeof(pWebSocket->strRawKey));
|
|
|
|
// now, generate accept header we must receive to validate response
|
|
|
|
// concatentate key with hardcoded GUID as per spec
|
|
ds_strnzcpy(strAccept, pWebSocket->strRawKey, sizeof(strAccept));
|
|
ds_strnzcat(strAccept, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", sizeof(strAccept));
|
|
|
|
// hash the key with SHA1
|
|
CryptSha1Init(&Sha1State);
|
|
CryptSha1Update(&Sha1State, (uint8_t *)strAccept, (uint32_t)strlen(strAccept));
|
|
CryptSha1Final(&Sha1State, aSha1Data, CRYPTSHA1_HASHSIZE);
|
|
|
|
// base64 encode for final accept key
|
|
Base64Encode2((char *)aSha1Data, CRYPTSHA1_HASHSIZE, pWebSocket->strEncKey, sizeof(pWebSocket->strEncKey));
|
|
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 2, "protowebsocket: [%p] raw key=%s\n", pWebSocket, pWebSocket->strRawKey));
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 2, "protowebsocket: [%p] enc key=%s\n", pWebSocket, pWebSocket->strEncKey));
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketFormatHeader
|
|
|
|
\Description
|
|
Format websocket handshake header
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pHost - websocket host
|
|
\Input *pUrl - websocket url
|
|
\Input iPort - protocol port
|
|
\Input iSecure - true=secure, else false
|
|
|
|
\Output
|
|
int32_t - positive=success, negative=failure
|
|
|
|
\Version 11/27/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketFormatHeader(ProtoWebSocketRefT *pWebSocket, const char *pHost, const char *pUrl, int32_t iPort, int32_t iSecure)
|
|
{
|
|
int32_t iBufSize = pWebSocket->iOutMax, iBufLen = 0;
|
|
|
|
// if no url specified use the minimum
|
|
if (*pUrl == '\0')
|
|
{
|
|
pUrl = "/";
|
|
}
|
|
|
|
iBufLen += ds_snzprintf(pWebSocket->pOutBuf+iBufLen, iBufSize-iBufLen, "GET %s HTTP/1.1\r\n", pUrl);
|
|
if ((iSecure && (iPort == 443)) || (iPort == 80))
|
|
{
|
|
iBufLen += ds_snzprintf(pWebSocket->pOutBuf+iBufLen, iBufSize-iBufLen, "Host: %s\r\n", pHost);
|
|
}
|
|
else
|
|
{
|
|
iBufLen += ds_snzprintf(pWebSocket->pOutBuf+iBufLen, iBufSize-iBufLen, "Host: %s:%d\r\n", pHost, iPort);
|
|
}
|
|
iBufLen += ds_snzprintf(pWebSocket->pOutBuf+iBufLen, iBufSize-iBufLen, "Upgrade: websocket\r\n");
|
|
iBufLen += ds_snzprintf(pWebSocket->pOutBuf+iBufLen, iBufSize-iBufLen, "Connection: upgrade\r\n");
|
|
iBufLen += ds_snzprintf(pWebSocket->pOutBuf+iBufLen, iBufSize-iBufLen, "Sec-WebSocket-Key: %s\r\n", pWebSocket->strRawKey);
|
|
iBufLen += ds_snzprintf(pWebSocket->pOutBuf+iBufLen, iBufSize-iBufLen, "Sec-WebSocket-Version: %d\r\n", pWebSocket->iVersion);
|
|
if ((pWebSocket->pAppendHdr == NULL) || !ds_stristr(pWebSocket->pAppendHdr, "User-Agent:"))
|
|
{
|
|
iBufLen += ds_snzprintf(pWebSocket->pOutBuf+iBufLen, iBufSize-iBufLen, "User-Agent: ProtoWebSocket %d.%d/DS %d.%d.%d.%d.%d (" DIRTYCODE_PLATNAME ")\r\n",
|
|
(PROTOWEBSOCKET_VERSION>>8)&0xff, PROTOWEBSOCKET_VERSION&0xff, DIRTYSDK_VERSION_YEAR, DIRTYSDK_VERSION_SEASON,
|
|
DIRTYSDK_VERSION_MAJOR, DIRTYSDK_VERSION_MINOR, DIRTYSDK_VERSION_PATCH);
|
|
}
|
|
if ((pWebSocket->pAppendHdr == NULL) || (pWebSocket->pAppendHdr[0] == '\0'))
|
|
{
|
|
iBufLen += ds_snzprintf(pWebSocket->pOutBuf+iBufLen, iBufSize-iBufLen, "Accept: */*\r\n");
|
|
}
|
|
else
|
|
{
|
|
iBufLen += ds_snzprintf(pWebSocket->pOutBuf+iBufLen, iBufSize-iBufLen, "%s", pWebSocket->pAppendHdr);
|
|
}
|
|
iBufLen += ds_snzprintf(pWebSocket->pOutBuf+iBufLen, iBufSize-iBufLen, "\r\n");
|
|
pWebSocket->iOutLen = iBufLen;
|
|
pWebSocket->iOutOff = 0;
|
|
return(1);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketDisconnect
|
|
|
|
\Description
|
|
Disconnect from server
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input iNewState - new state to set (fail or disc)
|
|
|
|
\Version 11/30/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketDisconnect(ProtoWebSocketRefT *pWebSocket, int32_t iNewState)
|
|
{
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 0, "protowebsocket: [%p] disconnecting\n", pWebSocket));
|
|
pWebSocket->eState = iNewState;
|
|
if (iNewState == ST_FAIL)
|
|
{
|
|
pWebSocket->iSSLFail = ProtoSSLStat(pWebSocket->pProtoSSL, 'fail', NULL, 0);
|
|
}
|
|
return(ProtoSSLDisconnect(pWebSocket->pProtoSSL));
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketReset
|
|
|
|
\Description
|
|
Reset module state
|
|
|
|
\Input *pWebSocket - module state
|
|
|
|
\Version 11/30/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static void _ProtoWebSocketReset(ProtoWebSocketRefT *pWebSocket)
|
|
{
|
|
// if we're not already disconnected, disconnect now
|
|
if (pWebSocket->eState != ST_DISC && pWebSocket->eState != ST_FAIL)
|
|
{
|
|
_ProtoWebSocketDisconnect(pWebSocket, ST_DISC);
|
|
}
|
|
|
|
// reset state
|
|
pWebSocket->iOutLen = 0;
|
|
pWebSocket->iOutOff = 0;
|
|
pWebSocket->iInpLen = 0;
|
|
pWebSocket->iInpOff = 0;
|
|
pWebSocket->iRecvRslt = 0;
|
|
pWebSocket->iRecvSize = 0;
|
|
pWebSocket->iFrameHdrOff = 0;
|
|
pWebSocket->iFrameHdrLen = 2; // minimal header size
|
|
pWebSocket->iFrameLen = 0;
|
|
pWebSocket->iFrameType = 0;
|
|
pWebSocket->iCtrlDataLen = 0;
|
|
pWebSocket->iSSLFail = 0;
|
|
pWebSocket->bTimeout = FALSE;
|
|
pWebSocket->bDoPong = FALSE;
|
|
pWebSocket->bDoClose = FALSE;
|
|
pWebSocket->bClosing = FALSE;
|
|
pWebSocket->bFrameFrag = FALSE;
|
|
pWebSocket->bFrameFin = FALSE;
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketFail
|
|
|
|
\Description
|
|
Handle a socket failure
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input iResult - result from a socket operation
|
|
\Input *pStrDebug - debug text indicating failure context
|
|
|
|
\Version 11/27/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static void _ProtoWebSocketFail(ProtoWebSocketRefT *pWebSocket, int32_t iResult, const char *pStrDebug)
|
|
{
|
|
if (pWebSocket->eState == ST_FAIL)
|
|
{
|
|
return;
|
|
}
|
|
NetPrintf(("protowebsocket: [%p] failure: %s (err=%d)\n", pWebSocket, pStrDebug, iResult));
|
|
_ProtoWebSocketDisconnect(pWebSocket, ST_FAIL);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketValidateHeader
|
|
|
|
\Description
|
|
Validate server response header
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pBuffer - buffer holding received header
|
|
\Input *pHeader - name of header to validate
|
|
\Input *pValue - header value to validate
|
|
|
|
\Output
|
|
int32_t - zero=success, negative=failure
|
|
|
|
\Version 11/27/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketValidateHeader(ProtoWebSocketRefT *pWebSocket, const char *pBuffer, const char *pHeader, const char *pValue)
|
|
{
|
|
char strDebug[256];
|
|
char strHdrVal[128];
|
|
|
|
if (ProtoHttpGetHeaderValue(NULL, pBuffer, pHeader, strHdrVal, sizeof(strHdrVal), NULL) < 0)
|
|
{
|
|
ds_snzprintf(strDebug, sizeof(strDebug), "missing %s header", pHeader);
|
|
_ProtoWebSocketFail(pWebSocket, -1, strDebug);
|
|
return(-1);
|
|
}
|
|
else if (ds_stricmp(strHdrVal, pValue))
|
|
{
|
|
ds_snzprintf(strDebug, sizeof(strDebug), "invalid %s header value '%s'", pHeader, pValue);
|
|
_ProtoWebSocketFail(pWebSocket, -1, strDebug);
|
|
return(-1);
|
|
}
|
|
return(0);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketProcessHeader
|
|
|
|
\Description
|
|
Process websocket handshake server response
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pBuffer - buffer holding received header
|
|
|
|
\Output
|
|
int32_t - positive=success, negative=failure
|
|
|
|
\Version 11/27/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketProcessHeader(ProtoWebSocketRefT *pWebSocket, const char *pBuffer)
|
|
{
|
|
int32_t iHdrCode;
|
|
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 1, "protowebsocket: [%p] handshake response received\n", pWebSocket));
|
|
#if DIRTYCODE_LOGGING
|
|
if (pWebSocket->iVerbose > 2)
|
|
{
|
|
NetPrintWrap(pBuffer, 80);
|
|
}
|
|
#endif
|
|
|
|
/* as per https://www.w3.org/TR/websockets/ 4.8: When the user agent validates the server's response during
|
|
the "establish a WebSocket connection" algorithm, if the status code received from the server is not 101
|
|
(e.g. it is a redirect), the user agent must fail the WebSocket connection. */
|
|
|
|
// validate 101 response
|
|
iHdrCode = ProtoHttpParseHeaderCode(pBuffer);
|
|
if (iHdrCode != 101)
|
|
{
|
|
_ProtoWebSocketFail(pWebSocket, iHdrCode, "invalid http response code");
|
|
return(-1);
|
|
}
|
|
// validate "upgrade: websocket" header
|
|
if (_ProtoWebSocketValidateHeader(pWebSocket, pBuffer, "upgrade", "websocket") < 0)
|
|
{
|
|
return(-2);
|
|
}
|
|
// validate "connection: upgrade" header
|
|
if (_ProtoWebSocketValidateHeader(pWebSocket, pBuffer, "connection", "upgrade") < 0)
|
|
{
|
|
return(-3);
|
|
}
|
|
// validate accept header
|
|
if (_ProtoWebSocketValidateHeader(pWebSocket, pBuffer, "sec-websocket-accept", pWebSocket->strEncKey) < 0)
|
|
{
|
|
return(-4);
|
|
}
|
|
|
|
//$$TODO - support Sec-WebSocket-Extensions?
|
|
//$$TODO - support Sec-WebSocket-Protocol?
|
|
|
|
pWebSocket->eState = ST_OPEN;
|
|
return(0);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketSend
|
|
|
|
\Description
|
|
Try and send some data. If data is sent, the timout value is updated.
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pStrBuf - pointer to buffer to send from
|
|
\Input iSize - amount of data to try and send
|
|
|
|
\Output
|
|
int32_t - negative=error, else number of bytes sent
|
|
|
|
\Version 11/27/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketSend(ProtoWebSocketRefT *pWebSocket, const char *pStrBuf, int32_t iSize)
|
|
{
|
|
int32_t iResult;
|
|
|
|
// try and send some data
|
|
if ((iResult = ProtoSSLSend(pWebSocket->pProtoSSL, pStrBuf, iSize)) > 0)
|
|
{
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 1, "protowebsocket: [%p] sent %d bytes\n", pWebSocket, iResult));
|
|
#if DIRTYCODE_LOGGING
|
|
if (pWebSocket->iVerbose > 3)
|
|
{
|
|
NetPrintMem(pStrBuf, iResult, "ws-send");
|
|
}
|
|
#endif
|
|
|
|
// sent data, so update timer
|
|
pWebSocket->uTimer = NetTick();
|
|
}
|
|
else if (iResult < 0)
|
|
{
|
|
NetPrintf(("protowebsocket: [%p] error %d sending %d bytes\n", pWebSocket, iResult, iSize));
|
|
pWebSocket->eState = ST_FAIL;
|
|
}
|
|
return(iResult);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketSendBuff
|
|
|
|
\Description
|
|
Send buffered data
|
|
|
|
\Input *pWebSocket - module state
|
|
|
|
\Output
|
|
int32_t - zero=in progress, positive=done, negative=failure
|
|
|
|
\Version 11/29/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketSendBuff(ProtoWebSocketRefT *pWebSocket)
|
|
{
|
|
int32_t iResult;
|
|
if ((iResult = _ProtoWebSocketSend(pWebSocket, pWebSocket->pOutBuf+pWebSocket->iOutOff, pWebSocket->iOutLen-pWebSocket->iOutOff)) > 0)
|
|
{
|
|
pWebSocket->iOutOff += iResult;
|
|
if (pWebSocket->iOutOff == pWebSocket->iOutLen)
|
|
{
|
|
iResult = pWebSocket->iOutLen;
|
|
pWebSocket->iOutLen = pWebSocket->iOutOff = 0;
|
|
}
|
|
else
|
|
{
|
|
iResult = 0;
|
|
}
|
|
}
|
|
return(iResult);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketSetLength
|
|
|
|
\Description
|
|
Endode message length in the format websocket protocol reqires
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pBuffer - buffer to encode to
|
|
\Input iMask - true if data is masked, else false
|
|
\Input iLength - length to set
|
|
|
|
\Output
|
|
uint8_t * - pointer past end of encoded length
|
|
|
|
\Notes
|
|
While the standard supports up to a 64-bit frame size, this implementation
|
|
only supports up to 32-bit frame sizes.
|
|
|
|
\Version 11/29/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static uint8_t *_ProtoWebSocketSetLength(ProtoWebSocketRefT *pWebSocket, uint8_t *pBuffer, int32_t iMask, int32_t iLength)
|
|
{
|
|
iMask = iMask ? 0x80 : 0;
|
|
if (iLength < 126)
|
|
{
|
|
*pBuffer++ = (uint8_t)iLength | (uint8_t)iMask;
|
|
}
|
|
else if (iLength < 65536)
|
|
{
|
|
*pBuffer++ = (uint8_t)126 | (uint8_t)iMask;
|
|
*pBuffer++ = (uint8_t)(iLength >> 8);
|
|
*pBuffer++ = (uint8_t)(iLength >> 0);
|
|
}
|
|
else
|
|
{
|
|
*pBuffer++ = (uint8_t)127 | (uint8_t)iMask;
|
|
*pBuffer++ = 0;
|
|
*pBuffer++ = 0;
|
|
*pBuffer++ = 0;
|
|
*pBuffer++ = 0;
|
|
*pBuffer++ = (uint8_t)(iLength >> 24);
|
|
*pBuffer++ = (uint8_t)(iLength >> 16);
|
|
*pBuffer++ = (uint8_t)(iLength >> 8);
|
|
*pBuffer++ = (uint8_t)(iLength >> 0);
|
|
}
|
|
return(pBuffer);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketSetMask
|
|
|
|
\Description
|
|
Generate and encode mask; required for sending data from client to server
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pBuffer - [out] buffer to write mask to
|
|
\Input **ppMask - [out] storage for pointer to mask
|
|
|
|
\Output
|
|
uint8_t * - pointer past end of encoded mask
|
|
|
|
\Version 11/29/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static uint8_t *_ProtoWebSocketSetMask(ProtoWebSocketRefT *pWebSocket, uint8_t *pBuffer, uint8_t **ppMask)
|
|
{
|
|
// generate four-byte random mask
|
|
CryptRandGet(pBuffer, 4);
|
|
// save pointer to mask for caller
|
|
*ppMask = pBuffer;
|
|
// print mask for extended diagnostics
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 2, "protowebsocket: [%p] mask=0x%02x%02x%02x%02x\n", pWebSocket,
|
|
pBuffer[0], pBuffer[1], pBuffer[2], pBuffer[3]));
|
|
return(pBuffer+4);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketSendData
|
|
|
|
\Description
|
|
Send masked data from client to server.
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pBuffer - data to send
|
|
\Input iLength - length of data to send
|
|
\Input uOpcode - frame type
|
|
|
|
\Output
|
|
int32_t - number of data bytes sent
|
|
|
|
\Version 11/29/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketSendData(ProtoWebSocketRefT *pWebSocket, const char *pBuffer, int32_t iLength, uint8_t uOpcode)
|
|
{
|
|
uint8_t *pPacket = (uint8_t *)pWebSocket->pOutBuf, *pMask;
|
|
int32_t iBufIdx, iOutMax, iResult;
|
|
uint8_t uFrameFin = PROTOWEBSOCKET_OPCODE_FIN;
|
|
|
|
// only allow if there's not a send in progress
|
|
if (pWebSocket->iOutLen != 0)
|
|
{
|
|
return(0);
|
|
}
|
|
|
|
// opcode is only set for the first packet of a fragmented message
|
|
if (pWebSocket->bMessageSend && pWebSocket->bFrameFrag)
|
|
{
|
|
uOpcode = 0;
|
|
}
|
|
|
|
// calculate available space, while reserving max header size
|
|
iOutMax = pWebSocket->iOutMax - PROTOWEBSOCKET_MAXSENDHDR;
|
|
if (iLength > iOutMax)
|
|
{
|
|
// for message sending, clear FIN bit when it isn't the final frame
|
|
if (pWebSocket->bMessageSend)
|
|
{
|
|
uFrameFin = 0;
|
|
pWebSocket->bFrameFrag = TRUE;
|
|
}
|
|
// constrain send length to max available space
|
|
iLength = iOutMax;
|
|
}
|
|
else if (pWebSocket->bMessageSend)
|
|
{
|
|
// reset fragmentation flag on final frame of message
|
|
pWebSocket->bFrameFrag = FALSE;
|
|
}
|
|
|
|
// set packet opcode
|
|
*pPacket++ = uOpcode|uFrameFin;
|
|
// set packet length
|
|
pPacket = _ProtoWebSocketSetLength(pWebSocket, pPacket, 1, iLength);
|
|
// set packet mask
|
|
pPacket = _ProtoWebSocketSetMask(pWebSocket, pPacket, &pMask);
|
|
|
|
// mask the data
|
|
for (iBufIdx = 0; iBufIdx < iLength; iBufIdx += 1)
|
|
{
|
|
*pPacket++ = *pBuffer++ ^ pMask[iBufIdx&0x3];
|
|
}
|
|
|
|
// set buffer parms
|
|
pWebSocket->iOutOff = 0;
|
|
pWebSocket->iOutLen = (int32_t)(pPacket-(uint8_t *)pWebSocket->pOutBuf);
|
|
|
|
// try to send
|
|
if ((iResult = _ProtoWebSocketSendBuff(pWebSocket)) < 0)
|
|
{
|
|
return(iResult);
|
|
}
|
|
|
|
#if DIRTYCODE_LOGGING
|
|
if (iResult > 0)
|
|
{
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 1, "protowebsocket: [%p] sent frame fin=%d typ=%s len=%d\n", pWebSocket, ((uint8_t *)pWebSocket->pOutBuf)[0]>>7,
|
|
_ProtoWebSocket_strOpcodeNames[((uint8_t *)pWebSocket->pOutBuf)[0]&0xf], iResult));
|
|
}
|
|
#endif
|
|
|
|
|
|
// return amount of data buffered
|
|
return(iLength);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketSendUserData
|
|
|
|
\Description
|
|
Send masked data from client to server making, wrapping the state error
|
|
checking
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pBuffer - data to send
|
|
\Input iLength - length of data to send
|
|
\Input uOpcode - frame type
|
|
|
|
\Output
|
|
int32_t - number of data bytes sent
|
|
|
|
\Version 08/30/2017 (eesponda)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketSendUserData(ProtoWebSocketRefT *pWebSocket, const char *pBuffer, int32_t iLength, uint8_t uOpcode)
|
|
{
|
|
// handle not connected states
|
|
if ((pWebSocket->eState == ST_DISC) || (pWebSocket->eState == ST_FAIL))
|
|
{
|
|
return(SOCKERR_NOTCONN);
|
|
}
|
|
if (pWebSocket->eState != ST_OPEN)
|
|
{
|
|
return(0);
|
|
}
|
|
return(_ProtoWebSocketSendData(pWebSocket, pBuffer, iLength, uOpcode));
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketSendClose
|
|
|
|
\Description
|
|
Send a close notification to server (or queue it if we are in the middle
|
|
of another send).
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input iReason - (optional) close reason (PROTOWEBSOCKET_CLOSEREASON_*)
|
|
\Input *pStrReason - (optional) string close reason
|
|
|
|
\Output
|
|
int32_t - number of bytes data sent
|
|
|
|
\Version 11/30/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketSendClose(ProtoWebSocketRefT *pWebSocket, int32_t iReason, const char *pStrReason)
|
|
{
|
|
int32_t iDataLen, iResult;
|
|
|
|
// if we've already sent a close frame, we don't want to send another
|
|
if (pWebSocket->bClosing)
|
|
{
|
|
return(0);
|
|
}
|
|
pWebSocket->bClosing = TRUE;
|
|
|
|
// format reason data
|
|
if (iReason != 0)
|
|
{
|
|
iDataLen = 2;
|
|
pWebSocket->strCtrlData[0] = (iReason>>8)&0xff;
|
|
pWebSocket->strCtrlData[1] = iReason&0xff;
|
|
if (pStrReason != NULL)
|
|
{
|
|
ds_strnzcpy(pWebSocket->strCtrlData+2, pStrReason, sizeof(pWebSocket->strCtrlData)-2);
|
|
iDataLen += (int32_t)strlen(pWebSocket->strCtrlData+2);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pWebSocket->strCtrlData[0] = '\0';
|
|
iDataLen = 0;
|
|
}
|
|
|
|
// try immediate send
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 0, "protowebsocket: [%p] closing connection (code=%d, text=%s)\n", pWebSocket, iReason, pStrReason));
|
|
if ((iResult = _ProtoWebSocketSendData(pWebSocket, pWebSocket->strCtrlData, iDataLen, PROTOWEBSOCKET_CTRL_CLSE)) == 0)
|
|
{
|
|
// couldn't buffer for sending, so set up to try again
|
|
pWebSocket->bDoClose = TRUE;
|
|
pWebSocket->iCtrlDataLen = iDataLen;
|
|
}
|
|
return(iResult);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketSendPing
|
|
|
|
\Description
|
|
Send a ping notification to server -- unlike close, we don't queue for
|
|
later sending if we can't send immediately.
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pStrData - (optional) string ping data
|
|
\Input iType - PING or PONG
|
|
|
|
\Output
|
|
int32_t - number of bytes data sent
|
|
|
|
\Version 11/30/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketSendPing(ProtoWebSocketRefT *pWebSocket, const char *pStrData, int32_t iType)
|
|
{
|
|
int32_t iDataLen = 0;
|
|
if (pStrData != NULL)
|
|
{
|
|
iDataLen = (int32_t)strlen(pStrData);
|
|
}
|
|
return(_ProtoWebSocketSendData(pWebSocket, pStrData, iDataLen, iType));
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketRecv
|
|
|
|
\Description
|
|
Try and receive some data. If data is received, the timout value is
|
|
updated.
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pStrBuf - [out] pointer to buffer to receive into
|
|
\Input iSize - amount of data to try and receive
|
|
|
|
\Output
|
|
int32_t - negative=error, else success
|
|
|
|
\Version 11/27/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketRecv(ProtoWebSocketRefT *pWebSocket, char *pStrBuf, int32_t iSize)
|
|
{
|
|
// if we have no buffer space, don't try to receive
|
|
if (iSize == 0)
|
|
{
|
|
return(0);
|
|
}
|
|
|
|
/* if we are receiving when in the RECV state or if we have no data already waiting
|
|
pull from the TCP buffer. when we are in the RECV state we need to pull into the temp
|
|
input buffer, hence why we need the state check */
|
|
if ((pWebSocket->eState == ST_RECV) || (pWebSocket->iInpLen == 0))
|
|
{
|
|
pWebSocket->iRecvRslt = ProtoSSLRecv(pWebSocket->pProtoSSL, pStrBuf, iSize);
|
|
}
|
|
// otherwise pull from the temp input buffer
|
|
else
|
|
{
|
|
iSize = DS_MIN(iSize, pWebSocket->iInpLen-pWebSocket->iInpOff);
|
|
ds_memcpy(pStrBuf, pWebSocket->pInpBuf+pWebSocket->iInpOff, iSize);
|
|
pWebSocket->iInpOff += iSize;
|
|
|
|
// if we read the entire buffer, reset the variables
|
|
if (pWebSocket->iInpLen == pWebSocket->iInpOff)
|
|
{
|
|
pWebSocket->iInpLen = pWebSocket->iInpOff = 0;
|
|
}
|
|
pWebSocket->iRecvRslt = iSize;
|
|
}
|
|
|
|
// handle the result
|
|
if (pWebSocket->iRecvRslt > 0)
|
|
{
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 1, "protowebsocket: [%p] recv %d bytes\n", pWebSocket, pWebSocket->iRecvRslt));
|
|
#if DIRTYCODE_LOGGING
|
|
if (pWebSocket->iVerbose > 3)
|
|
{
|
|
NetPrintMem(pStrBuf, pWebSocket->iRecvRslt, "ws-recv");
|
|
}
|
|
#endif
|
|
|
|
// received data, so update timer
|
|
pWebSocket->uTimer = NetTick();
|
|
}
|
|
return(pWebSocket->iRecvRslt);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketCheckCtrl
|
|
|
|
\Description
|
|
Check a control frame received from the server
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pStrBuf - pointer to buffer containing received data from server
|
|
\Input iSize - amount of data to received from the server
|
|
|
|
\Version 11/29/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static void _ProtoWebSocketCheckCtrl(ProtoWebSocketRefT *pWebSocket, char *pStrBuf, int32_t iSize)
|
|
{
|
|
if (pWebSocket->iFrameType == PROTOWEBSOCKET_CTRL_PING)
|
|
{
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 1, "protowebsocket: [%p] got a ping with %d bytes of data\n", pWebSocket, iSize));
|
|
// set to respond with a pong
|
|
pWebSocket->bDoPong = TRUE;
|
|
}
|
|
else if (pWebSocket->iFrameType == PROTOWEBSOCKET_CTRL_PONG)
|
|
{
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 1, "protowebsocket: [%p] got a pong with %d bytes of data\n", pWebSocket, iSize));
|
|
}
|
|
else if (pWebSocket->iFrameType == PROTOWEBSOCKET_CTRL_CLSE)
|
|
{
|
|
int32_t iCloseReason = ((uint8_t)pStrBuf[0]) << 8 | (uint8_t)pStrBuf[1];
|
|
if ((iCloseReason >= PROTOWEBSOCKET_CLOSEREASON_BASE) && (iCloseReason <= PROTOWEBSOCKET_CLOSEREASON_MAX))
|
|
{
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 0, "protowebsocket: [%p] got a close with reason '%s' (len=%d)\n", pWebSocket,
|
|
_ProtoWebSocket_strCloseReasons[iCloseReason-PROTOWEBSOCKET_CLOSEREASON_BASE], iSize));
|
|
pWebSocket->iCloseReason = iCloseReason;
|
|
}
|
|
else
|
|
{
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 0, "protowebsocket: [%p] got a close with unknown reason %d (len=%d)\n", pWebSocket,
|
|
iCloseReason, iSize));
|
|
pWebSocket->iCloseReason = PROTOWEBSOCKET_CLOSEREASON_UNKNOWN;
|
|
}
|
|
// disconnect
|
|
_ProtoWebSocketDisconnect(pWebSocket, ST_DISC);
|
|
}
|
|
else
|
|
{
|
|
NetPrintf(("protowebsocket: [%p] ignoring unknown control type %d (len=%d)\n", pWebSocket, pWebSocket->iFrameType, iSize));
|
|
// ignore unknown frame type data?
|
|
}
|
|
|
|
// done with control data, reset frame header info; this is only necessary for control messages with no associated data
|
|
pWebSocket->iFrameHdrOff = 0;
|
|
pWebSocket->iFrameHdrLen = 2; // minimum header size
|
|
pWebSocket->bFrameFin = FALSE;
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketCheckHeader
|
|
|
|
\Description
|
|
Check a server->client header for completion
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pStrBuf - pointer to buffer containing received data from server
|
|
\Input iSize - amount of data received from the server
|
|
|
|
\Output
|
|
int32_t - zero=in progress, else complete
|
|
|
|
\Notes
|
|
While the standard supports up to a 64-bit frame size, this implementation
|
|
only supports up to 32-bit frame sizes.
|
|
|
|
\Version 01/09/2013 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketCheckHeader(ProtoWebSocketRefT *pWebSocket, char *pStrBuf, int32_t iSize)
|
|
{
|
|
// check for minimum frame size
|
|
if (iSize < 2)
|
|
{
|
|
return(0);
|
|
}
|
|
// decode frame info
|
|
pWebSocket->bFrameFin = ((uint8_t *)pStrBuf)[0] >> 7;
|
|
pWebSocket->iFrameType = pStrBuf[0]&0x0f;
|
|
pWebSocket->iFrameLen = pStrBuf[1];
|
|
// handle extended frame lengths
|
|
if (pWebSocket->iFrameLen == 126)
|
|
{
|
|
pWebSocket->iFrameHdrLen = 4;
|
|
}
|
|
else if (pWebSocket->iFrameLen == 127)
|
|
{
|
|
pWebSocket->iFrameHdrLen = 10;
|
|
}
|
|
// check extended header size
|
|
if (iSize < pWebSocket->iFrameHdrLen)
|
|
{
|
|
return(0);
|
|
}
|
|
// decode extended frame lengths
|
|
if (pWebSocket->iFrameLen == 126)
|
|
{
|
|
pWebSocket->iFrameLen = (((uint8_t *)pStrBuf)[2] << 8) | ((uint8_t *)pStrBuf)[3];
|
|
}
|
|
else if (pWebSocket->iFrameLen == 127)
|
|
{
|
|
pWebSocket->iFrameLen = (((uint8_t *)pStrBuf)[6] << 24) | (((uint8_t *)pStrBuf)[7] << 16) | (((uint8_t *)pStrBuf)[8] << 8) | ((uint8_t *)pStrBuf)[9];
|
|
}
|
|
// if it's a control frame, set up to process it
|
|
if (pWebSocket->iFrameType & PROTOWEBSOCKET_CTRL_BASE)
|
|
{
|
|
pWebSocket->iCtrlDataLen = pWebSocket->iFrameLen;
|
|
pWebSocket->iCtrlDataOff = 0;
|
|
}
|
|
return(1);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketRecvHeader
|
|
|
|
\Description
|
|
Receive a server->client frame header. As headers are variable length
|
|
and we receive data directly into the user-supplied buffer, we process
|
|
the header separately from the data.
|
|
|
|
\Input *pWebSocket - module state
|
|
|
|
\Output
|
|
int32_t - negative=error, zero=in progress, positive=done
|
|
|
|
\Version 01/09/2013 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketRecvHeader(ProtoWebSocketRefT *pWebSocket)
|
|
{
|
|
int32_t iResult;
|
|
|
|
// receive first two header bytes
|
|
while ((iResult = _ProtoWebSocketRecv(pWebSocket, pWebSocket->strFrameHdr+pWebSocket->iFrameHdrOff, pWebSocket->iFrameHdrLen-pWebSocket->iFrameHdrOff)) > 0)
|
|
{
|
|
// accumulate result
|
|
pWebSocket->iFrameHdrOff += iResult;
|
|
// see if we have a valid header
|
|
if ((iResult = _ProtoWebSocketCheckHeader(pWebSocket, pWebSocket->strFrameHdr, pWebSocket->iFrameHdrOff)) > 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
#if DIRTYCODE_LOGGING
|
|
if (iResult > 0)
|
|
{
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 1, "protowebsocket: [%p] recv frame fin=%d typ=%s len=%d\n", pWebSocket, ((uint8_t *)pWebSocket->strFrameHdr)[0]>>7,
|
|
_ProtoWebSocket_strOpcodeNames[pWebSocket->iFrameType], pWebSocket->iFrameLen));
|
|
}
|
|
#endif
|
|
return(iResult);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketRecvCtrl
|
|
|
|
\Description
|
|
Receive a control packet into internal buffer. Any data that doesn't
|
|
fit in the internal buffer is discarded.
|
|
|
|
\Input *pWebSocket - module state
|
|
|
|
\Output
|
|
int32_t - zero
|
|
|
|
\Version 02/01/2013 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketRecvCtrl(ProtoWebSocketRefT *pWebSocket)
|
|
{
|
|
char strTempBuf[256], *pStrBuf;
|
|
int32_t iSize;
|
|
|
|
// receive into control data cache buffer (if we have enough room)
|
|
if (pWebSocket->iCtrlDataOff < (signed)sizeof(pWebSocket->strCtrlData))
|
|
{
|
|
pStrBuf = pWebSocket->strCtrlData + pWebSocket->iCtrlDataOff;
|
|
iSize = (signed)sizeof(pWebSocket->strCtrlData) - pWebSocket->iCtrlDataOff;
|
|
}
|
|
else // no room, so just receive into stack buffer and throw out
|
|
{
|
|
pStrBuf = strTempBuf;
|
|
iSize = (signed)sizeof(strTempBuf);
|
|
}
|
|
pWebSocket->bRecvCtrlData = TRUE;
|
|
pWebSocket->iCtrlDataOff += _ProtoWebSocketRecvData(pWebSocket, pStrBuf, iSize);
|
|
pWebSocket->bRecvCtrlData = FALSE;
|
|
|
|
// process if we've received the whole control frame
|
|
if ((pWebSocket->iCtrlDataOff == pWebSocket->iCtrlDataLen) && (pStrBuf != strTempBuf))
|
|
{
|
|
_ProtoWebSocketCheckCtrl(pWebSocket, pStrBuf, pWebSocket->iCtrlDataLen);
|
|
}
|
|
return(0);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketRecvData
|
|
|
|
\Description
|
|
Try and receive some data.
|
|
|
|
\Input *pWebSocket - module state
|
|
\Input *pStrBuf - [out] pointer to buffer to receive into
|
|
\Input iSize - amount of data to try and receive
|
|
|
|
\Output
|
|
int32_t - negative=error, else success
|
|
|
|
\Version 11/27/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static int32_t _ProtoWebSocketRecvData(ProtoWebSocketRefT *pWebSocket, char *pStrBuf, int32_t iSize)
|
|
{
|
|
int32_t iResult;
|
|
|
|
// receive header
|
|
if ((pWebSocket->iFrameLen == 0) && ((iResult = _ProtoWebSocketRecvHeader(pWebSocket)) <= 0))
|
|
{
|
|
return(iResult);
|
|
}
|
|
|
|
// receive control frame data
|
|
if ((pWebSocket->iFrameType & PROTOWEBSOCKET_CTRL_BASE) && (!pWebSocket->bRecvCtrlData))
|
|
{
|
|
return(_ProtoWebSocketRecvCtrl(pWebSocket));
|
|
}
|
|
|
|
// limit read maximum to frame len
|
|
if (iSize > pWebSocket->iFrameLen)
|
|
{
|
|
iSize = pWebSocket->iFrameLen;
|
|
}
|
|
|
|
// receive some data
|
|
if ((iResult = _ProtoWebSocketRecv(pWebSocket, pStrBuf, iSize)) > 0)
|
|
{
|
|
// decrement frame size by amount of user data received
|
|
pWebSocket->iFrameLen -= iResult;
|
|
// if we've received all of the data, reset header info
|
|
if (pWebSocket->iFrameLen == 0)
|
|
{
|
|
pWebSocket->iFrameHdrOff = 0;
|
|
pWebSocket->iFrameHdrLen = 2; // minimum header size
|
|
}
|
|
}
|
|
else if (iResult < 0)
|
|
{
|
|
_ProtoWebSocketFail(pWebSocket, iResult, "receiving data");
|
|
}
|
|
return(iResult);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketProcessSendPong
|
|
|
|
\Description
|
|
Process a pending pong request
|
|
|
|
\Input *pWebSocket - module state
|
|
|
|
\Version 11/30/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static void _ProtoWebSocketProcessSendPong(ProtoWebSocketRefT *pWebSocket)
|
|
{
|
|
int32_t iResult;
|
|
if ((pWebSocket->bDoPong != TRUE) || (pWebSocket->iOutLen > 0))
|
|
{
|
|
return;
|
|
}
|
|
if ((iResult = _ProtoWebSocketSendPing(pWebSocket, pWebSocket->strCtrlData, PROTOWEBSOCKET_CTRL_PONG)) >= 0)
|
|
{
|
|
pWebSocket->bDoPong = FALSE;
|
|
}
|
|
else if (iResult < 0)
|
|
{
|
|
_ProtoWebSocketFail(pWebSocket, iResult, "sending pong");
|
|
pWebSocket->bDoPong = FALSE;
|
|
}
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketProcessSendClose
|
|
|
|
\Description
|
|
Process a pending close request.
|
|
|
|
\Input *pWebSocket - module state
|
|
|
|
\Version 11/30/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static void _ProtoWebSocketProcessSendClose(ProtoWebSocketRefT *pWebSocket)
|
|
{
|
|
int32_t iResult;
|
|
if ((pWebSocket->bDoClose != TRUE) || (pWebSocket->iOutLen > 0))
|
|
{
|
|
return;
|
|
}
|
|
if ((iResult = _ProtoWebSocketSendData(pWebSocket, pWebSocket->strCtrlData, pWebSocket->iCtrlDataLen, PROTOWEBSOCKET_CTRL_CLSE)) == 0)
|
|
{
|
|
pWebSocket->bDoClose = FALSE;
|
|
}
|
|
else if (iResult < 0)
|
|
{
|
|
_ProtoWebSocketFail(pWebSocket, iResult, "sending close");
|
|
pWebSocket->bDoClose = FALSE;
|
|
}
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function _ProtoWebSocketProcessSend
|
|
|
|
\Description
|
|
Process an in-progress send from the output buffer.
|
|
|
|
\Input *pWebSocket - module state
|
|
|
|
\Version 11/30/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
static void _ProtoWebSocketProcessSend(ProtoWebSocketRefT *pWebSocket)
|
|
{
|
|
int32_t iResult;
|
|
if (pWebSocket->iOutLen == 0)
|
|
{
|
|
return;
|
|
}
|
|
if ((iResult = _ProtoWebSocketSendBuff(pWebSocket)) < 0)
|
|
{
|
|
_ProtoWebSocketFail(pWebSocket, iResult, "sending data");
|
|
}
|
|
}
|
|
|
|
|
|
/*** Public functions *************************************************************/
|
|
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketCreate
|
|
|
|
\Description
|
|
Allocate a WebSocket connection and prepare for use
|
|
|
|
\Input iBufSize - websocket buffer size (determines max message size)
|
|
|
|
\Output
|
|
ProtoWebSocketRefT * - module state, or NULL on failure
|
|
|
|
\Version 11/26/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
ProtoWebSocketRefT *ProtoWebSocketCreate(int32_t iBufSize)
|
|
{
|
|
ProtoWebSocketRefT *pWebSocket;
|
|
int32_t iMemGroup;
|
|
void *pMemGroupUserData;
|
|
|
|
// Query mem group data
|
|
DirtyMemGroupQuery(&iMemGroup, &pMemGroupUserData);
|
|
|
|
// clamp the buffer size
|
|
if (iBufSize < PROTOWEBSOCKET_BUFSIZE_MIN)
|
|
{
|
|
iBufSize = PROTOWEBSOCKET_BUFSIZE_MIN;
|
|
}
|
|
iBufSize += PROTOWEBSOCKET_MAXSENDHDR;
|
|
|
|
// allocate and init module state
|
|
if ((pWebSocket = DirtyMemAlloc(sizeof(*pWebSocket), PROTOWEBSOCKET_MEMID, iMemGroup, pMemGroupUserData)) == NULL)
|
|
{
|
|
NetPrintf(("protowebsocket: could not allocate module state\n"));
|
|
return(NULL);
|
|
}
|
|
ds_memclr(pWebSocket, sizeof(*pWebSocket));
|
|
pWebSocket->iMemGroup = iMemGroup;
|
|
pWebSocket->pMemGroupUserData = pMemGroupUserData;
|
|
pWebSocket->iTimeout = PROTOWEBSOCKET_TIMEOUT;
|
|
pWebSocket->iVersion = PROTOWEBSOCKET_PROTOCOLVERS;
|
|
pWebSocket->iVerbose = 1;
|
|
|
|
// allocate ssl module
|
|
if ((pWebSocket->pProtoSSL = ProtoSSLCreate()) == NULL)
|
|
{
|
|
NetPrintf(("protowebsocket: [%p] unable to allocate ssl module\n", pWebSocket));
|
|
ProtoWebSocketDestroy(pWebSocket);
|
|
return(NULL);
|
|
}
|
|
// allocate websocket buffer
|
|
if ((pWebSocket->pOutBuf = DirtyMemAlloc(iBufSize, PROTOWEBSOCKET_MEMID, iMemGroup, pMemGroupUserData)) == NULL)
|
|
{
|
|
NetPrintf(("protowebsocket: [%p] unable to allocate websocket buffer\n", pWebSocket));
|
|
ProtoWebSocketDestroy(pWebSocket);
|
|
return(NULL);
|
|
}
|
|
pWebSocket->iOutMax = iBufSize;
|
|
|
|
// allocate temp input buffer
|
|
pWebSocket->iInpMax = PROTOWEBSOCKET_DEFAULT_INPUTBUFSIZE;
|
|
if ((pWebSocket->pInpBuf = (char *)DirtyMemAlloc(pWebSocket->iInpMax, PROTOWEBSOCKET_MEMID, iMemGroup, pMemGroupUserData)) == NULL)
|
|
{
|
|
NetPrintf(("protowebsocket: [%p] unable to allocate temporary input buffer\n", pWebSocket));
|
|
ProtoWebSocketDestroy(pWebSocket);
|
|
return(NULL);
|
|
}
|
|
|
|
// return module state to caller
|
|
return(pWebSocket);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketCreate2
|
|
|
|
\Description
|
|
Allocate a WebSocket connection and prepare for use: ProtoSSL function
|
|
signature compatible version.
|
|
|
|
\Output
|
|
ProtoWebSocketRefT * - module state, or NULL on failure
|
|
|
|
\Version 03/08/2013 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
ProtoWebSocketRefT *ProtoWebSocketCreate2(void)
|
|
{
|
|
return(ProtoWebSocketCreate(PROTOWEBSOCKET_BUFSIZE_MIN));
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketDestroy
|
|
|
|
\Description
|
|
Destroy a connection and release state
|
|
|
|
\Input *pWebSocket - websocket module state
|
|
|
|
\Version 11/26/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
void ProtoWebSocketDestroy(ProtoWebSocketRefT *pWebSocket)
|
|
{
|
|
if (pWebSocket->pAppendHdr != NULL)
|
|
{
|
|
DirtyMemFree(pWebSocket->pAppendHdr, PROTOWEBSOCKET_MEMID, pWebSocket->iMemGroup, pWebSocket->pMemGroupUserData);
|
|
}
|
|
if (pWebSocket->pInpBuf != NULL)
|
|
{
|
|
DirtyMemFree(pWebSocket->pInpBuf, PROTOWEBSOCKET_MEMID, pWebSocket->iMemGroup, pWebSocket->pMemGroupUserData);
|
|
}
|
|
if (pWebSocket->pOutBuf != NULL)
|
|
{
|
|
DirtyMemFree(pWebSocket->pOutBuf, PROTOWEBSOCKET_MEMID, pWebSocket->iMemGroup, pWebSocket->pMemGroupUserData);
|
|
}
|
|
if (pWebSocket->pProtoSSL != NULL)
|
|
{
|
|
ProtoSSLDestroy(pWebSocket->pProtoSSL);
|
|
}
|
|
DirtyMemFree(pWebSocket, PROTOWEBSOCKET_MEMID, pWebSocket->iMemGroup, pWebSocket->pMemGroupUserData);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketConnect
|
|
|
|
\Description
|
|
Initiate a connection to a server
|
|
|
|
\Input *pWebSocket - websocket module state
|
|
\Input *pUrl - url containing host information; ws:// or wss://
|
|
|
|
\Output
|
|
int32_t - zero=success, else failure
|
|
|
|
\Version 11/26/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
int32_t ProtoWebSocketConnect(ProtoWebSocketRefT *pWebSocket, const char *pUrl)
|
|
{
|
|
int32_t iSecure, iResult;
|
|
char strKind[16];
|
|
|
|
// reset module state
|
|
_ProtoWebSocketReset(pWebSocket);
|
|
|
|
// parse out the url
|
|
pUrl = ProtoHttpUrlParse(pUrl, strKind, sizeof(strKind), pWebSocket->strHost, sizeof(pWebSocket->strHost), &pWebSocket->iPort, &iSecure);
|
|
|
|
// do our own secure determination
|
|
iSecure = ds_stricmp(strKind, "wss") ? 0 : 1;
|
|
if (iSecure && (pWebSocket->iPort == 80))
|
|
{
|
|
pWebSocket->iPort = 443;
|
|
}
|
|
|
|
// create key we send and encded key we must validate in server response
|
|
_ProtoWebSocketGenerateKey(pWebSocket);
|
|
|
|
// format the request header for sending once connected
|
|
_ProtoWebSocketFormatHeader(pWebSocket, pWebSocket->strHost, pUrl, pWebSocket->iPort, iSecure);
|
|
|
|
// start the connect
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 0, "protowebsocket: [%p] connecting to %s:%d\n", pWebSocket, pWebSocket->strHost, pWebSocket->iPort));
|
|
iResult = ProtoSSLConnect(pWebSocket->pProtoSSL, iSecure, pWebSocket->strHost, 0, pWebSocket->iPort);
|
|
pWebSocket->eState = (iResult == SOCKERR_NONE) ? ST_CONN : ST_FAIL;
|
|
pWebSocket->uTimer = NetTick();
|
|
return(iResult);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketDisconnect
|
|
|
|
\Description
|
|
Disconnect from the server. Note that this is an asynchronous operation.
|
|
Connection status can be polled with the 'conn' status selector
|
|
|
|
\Input *pWebSocket - websocket module state
|
|
|
|
\Output
|
|
int32_t - zero=success, else failure
|
|
|
|
\Version 11/26/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
int32_t ProtoWebSocketDisconnect(ProtoWebSocketRefT *pWebSocket)
|
|
{
|
|
// ignore request in states where we are already disconnected
|
|
if ((pWebSocket->eState == ST_DISC) || (pWebSocket->eState == ST_FAIL))
|
|
{
|
|
return(0);
|
|
}
|
|
// if we're in handshaking just do an immediate close
|
|
if (pWebSocket->eState != ST_OPEN)
|
|
{
|
|
return(_ProtoWebSocketDisconnect(pWebSocket, ST_DISC));
|
|
}
|
|
// send close notification
|
|
return(_ProtoWebSocketSendClose(pWebSocket, PROTOWEBSOCKET_CLOSEREASON_NORMAL, NULL));
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketSend
|
|
|
|
\Description
|
|
Send data to a server over a WebSocket connection. Stream-type send, no
|
|
framing is performed by ProtoWebSocket.
|
|
|
|
\Input *pWebSocket - websocket module state
|
|
\Input *pBuffer - data to send
|
|
\Input iLength - amount of data to send
|
|
|
|
\Output
|
|
int32_t - negative=failure, else number of bytes of data sent
|
|
|
|
\Version 11/26/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
int32_t ProtoWebSocketSend(ProtoWebSocketRefT *pWebSocket, const char *pBuffer, int32_t iLength)
|
|
{
|
|
return(_ProtoWebSocketSendUserData(pWebSocket, pBuffer, iLength, PROTOWEBSOCKET_OPCODE_BINA));
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketSendText
|
|
|
|
\Description
|
|
Send text data to a server over a WebSocket connection. Stream-type send, no
|
|
framing is performed by ProtoWebSocket.
|
|
|
|
\Input *pWebSocket - websocket module state
|
|
\Input *pBuffer - data to send (NULL terminated C-style string)
|
|
|
|
\Output
|
|
int32_t - negative=failure, else number of bytes of data sent
|
|
|
|
\Version 08/30/2017 (eesponda)
|
|
*/
|
|
/********************************************************************************F*/
|
|
int32_t ProtoWebSocketSendText(ProtoWebSocketRefT *pWebSocket, const char *pBuffer)
|
|
{
|
|
return(_ProtoWebSocketSendUserData(pWebSocket, pBuffer, (int32_t)strlen(pBuffer), PROTOWEBSOCKET_OPCODE_TEXT));
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketSendMessage
|
|
|
|
\Description
|
|
Send a message to a server over a WebSocket connection. If the message
|
|
size is larger than the ProtoWebSocket buffer size, the message will be
|
|
sent fragmented.
|
|
|
|
\Input *pWebSocket - websocket module state
|
|
\Input *pBuffer - data to send
|
|
\Input iLength - amount of data to send
|
|
|
|
\Output
|
|
int32_t - negative=failure, else number of bytes of data sent
|
|
|
|
\Version 03/31/2017 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
int32_t ProtoWebSocketSendMessage(ProtoWebSocketRefT *pWebSocket, const char *pBuffer, int32_t iLength)
|
|
{
|
|
int32_t iResult;
|
|
pWebSocket->bMessageSend = TRUE;
|
|
iResult = _ProtoWebSocketSendUserData(pWebSocket, pBuffer, iLength, PROTOWEBSOCKET_OPCODE_BINA);
|
|
pWebSocket->bMessageSend = FALSE;
|
|
return(iResult);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketSendMessageText
|
|
|
|
\Description
|
|
Send a text message to a server over a WebSocket connection. If the message
|
|
size is larger than the ProtoWebSocket buffer size, the message will be
|
|
sent fragmented.
|
|
|
|
\Input *pWebSocket - websocket module state
|
|
\Input *pBuffer - data to send (NULL terminated C-style string)
|
|
|
|
\Output
|
|
int32_t - negative=failure, else number of bytes of data sent
|
|
|
|
\Version 08/30/2017 (eesponda)
|
|
*/
|
|
/********************************************************************************F*/
|
|
int32_t ProtoWebSocketSendMessageText(ProtoWebSocketRefT *pWebSocket, const char *pBuffer)
|
|
{
|
|
int32_t iResult;
|
|
pWebSocket->bMessageSend = TRUE;
|
|
iResult = _ProtoWebSocketSendUserData(pWebSocket, pBuffer, (int32_t)strlen(pBuffer), PROTOWEBSOCKET_OPCODE_TEXT);
|
|
pWebSocket->bMessageSend = FALSE;
|
|
return(iResult);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketRecv
|
|
|
|
\Description
|
|
Receive data from a server over a WebSocket connection. Stream-type recv,
|
|
framing bits are ignored.
|
|
|
|
\Input *pWebSocket - websocket module state
|
|
\Input *pBuffer - [out] buffer to receive data
|
|
\Input iLength - size of buffer
|
|
|
|
\Output
|
|
int32_t - negative=failure, else number of bytes of data received
|
|
|
|
\Version 11/26/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
int32_t ProtoWebSocketRecv(ProtoWebSocketRefT *pWebSocket, char *pBuffer, int32_t iLength)
|
|
{
|
|
// handle not connected states
|
|
if ((pWebSocket->eState == ST_DISC) || (pWebSocket->eState == ST_FAIL))
|
|
{
|
|
return(SOCKERR_NOTCONN);
|
|
}
|
|
if (pWebSocket->eState != ST_OPEN)
|
|
{
|
|
return(0);
|
|
}
|
|
|
|
// connected, do the receive
|
|
return(_ProtoWebSocketRecvData(pWebSocket, pBuffer, iLength));
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketRecvMessage
|
|
|
|
\Description
|
|
Receive a message from a server over a WebSocket connection. Note that
|
|
the user buffer is expected to persist and be capable of receiving the
|
|
entire message.
|
|
|
|
\Input *pWebSocket - websocket module state
|
|
\Input *pBuffer - [out] buffer to receive data
|
|
\Input iLength - size of buffer
|
|
|
|
\Output
|
|
int32_t - negative=failure, SOCKERR_NOMEM if the user buffer
|
|
is too small, zero=pending, else frame size received
|
|
|
|
\Version 03/30/2017 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
int32_t ProtoWebSocketRecvMessage(ProtoWebSocketRefT *pWebSocket, char *pBuffer, int32_t iLength)
|
|
{
|
|
int32_t iResult;
|
|
|
|
// see if we need more buffer
|
|
if (pWebSocket->iRecvSize == iLength)
|
|
{
|
|
return(SOCKERR_NOMEM);
|
|
}
|
|
|
|
// do the receive
|
|
if ((iResult = ProtoWebSocketRecv(pWebSocket, pBuffer+pWebSocket->iRecvSize, iLength-pWebSocket->iRecvSize)) > 0)
|
|
{
|
|
// accumulate receive size
|
|
pWebSocket->iRecvSize += iResult;
|
|
|
|
// return accumulated size if final frame and completely read, else return zero
|
|
if ((pWebSocket->bFrameFin) && (pWebSocket->iFrameLen == 0))
|
|
{
|
|
iResult = pWebSocket->iRecvSize;
|
|
pWebSocket->iRecvSize = 0;
|
|
pWebSocket->bFrameFin = FALSE;
|
|
}
|
|
else
|
|
{
|
|
iResult = 0;
|
|
}
|
|
}
|
|
|
|
// return result to caller
|
|
return(iResult);
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketStatus
|
|
|
|
\Description
|
|
Get module status
|
|
|
|
\Input *pWebSocket - websocket module state
|
|
\Input iSelect - status selector
|
|
\Input *pBuffer - [inp/out] buffer, selector specific
|
|
\Input iBufSize - size of buffer
|
|
|
|
\Output
|
|
int32_t - selector specific
|
|
|
|
\Notes
|
|
Selectors are:
|
|
|
|
\verbatim
|
|
SELECTOR RETURN RESULT
|
|
'crsn' returns close reason (PROTOWEBSOCKET_CLOSEREASON_*)
|
|
'essl' returns protossl error state
|
|
'host' current host copied to output buffer
|
|
'port' current port
|
|
'stat' connection status (-1=not connected, 0=connection in progress, 1=connected)
|
|
'time' TRUE if the client timed out the connection, else FALSE
|
|
\endverbatim
|
|
|
|
\Version 11/26/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
int32_t ProtoWebSocketStatus(ProtoWebSocketRefT *pWebSocket, int32_t iSelect, void *pBuffer, int32_t iBufSize)
|
|
{
|
|
// return close reason
|
|
if (iSelect == 'crsn')
|
|
{
|
|
if ((pBuffer != NULL) && (pWebSocket->iCtrlDataLen > 0))
|
|
{
|
|
int32_t iCopyLen = (iBufSize < pWebSocket->iCtrlDataLen) ? iBufSize : pWebSocket->iCtrlDataLen;
|
|
ds_memcpy(pBuffer, pWebSocket->strCtrlData, iCopyLen);
|
|
}
|
|
return(pWebSocket->iCloseReason);
|
|
}
|
|
|
|
// return protossl error state (we cache this since the state is reset when we disconnect on error)
|
|
if (iSelect == 'essl')
|
|
{
|
|
return(pWebSocket->iSSLFail);
|
|
}
|
|
|
|
// return current host
|
|
if (iSelect == 'host')
|
|
{
|
|
ds_strnzcpy(pBuffer, pWebSocket->strHost, iBufSize);
|
|
return(0);
|
|
}
|
|
|
|
// return current port
|
|
if (iSelect == 'port')
|
|
{
|
|
return(pWebSocket->iPort);
|
|
}
|
|
|
|
// check connection status
|
|
if (iSelect == 'stat')
|
|
{
|
|
if ((pWebSocket->eState == ST_DISC) || (pWebSocket->eState == ST_FAIL))
|
|
{
|
|
return(-1);
|
|
}
|
|
else if (pWebSocket->eState != ST_OPEN)
|
|
{
|
|
return(0);
|
|
}
|
|
// if we're connected (ST_OPEN) fall through to protossl connectivity check
|
|
}
|
|
|
|
// return timeout indicator
|
|
if (iSelect == 'time')
|
|
{
|
|
return(pWebSocket->bTimeout);
|
|
}
|
|
|
|
// pass through unhandled selectors to protossl
|
|
return(ProtoSSLStat(pWebSocket->pProtoSSL, iSelect, pBuffer, iBufSize));
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketControl
|
|
|
|
\Description
|
|
Control module behavior
|
|
|
|
\Input *pWebSocket - websocket module state
|
|
\Input iSelect - control selector
|
|
\Input iValue - selector specific
|
|
\Input iValue2 - selector specific
|
|
\Input *pValue - selector specific
|
|
|
|
\Output
|
|
int32_t - selector specific
|
|
|
|
\Notes
|
|
Selectors are:
|
|
|
|
\verbatim
|
|
SELECTOR DESCRIPTION
|
|
'apnd' The given buffer will be appended to future headers sent by
|
|
ProtoWebSocket. The Accept header in the default header will
|
|
be omitted. If there is a User-Agent header included in the
|
|
append header, it will replace the default User-Agent header.
|
|
'clse' Send a close message with optional reason (iValue) and data
|
|
(string passed in pValue)
|
|
'keep' Set keep-alive timer, in seconds; client sends a 'pong' at this
|
|
rate to keep connection alive (0 disables; disabled by default)
|
|
'ires' Resize the input buffer
|
|
'ping' Send a ping message with optional data (string passed in pValue)
|
|
'pong' Send a pong message with optional data (string passed in pValue)
|
|
'spam' Sets debug output verbosity (0...n)
|
|
'time' Sets ProtoWebSocket client timeout in seconds (default=300s)
|
|
\endverbatim
|
|
|
|
Unhandled selectors are passed on to ProtoSSL.
|
|
|
|
\Version 11/30/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
int32_t ProtoWebSocketControl(ProtoWebSocketRefT *pWebSocket, int32_t iSelect, int32_t iValue, int32_t iValue2, void *pValue)
|
|
{
|
|
if (iSelect == 'apnd')
|
|
{
|
|
return(_ProtoWebSocketSetAppendHeader(pWebSocket, (const char *)pValue));
|
|
}
|
|
if (iSelect == 'clse')
|
|
{
|
|
return(_ProtoWebSocketSendClose(pWebSocket, iValue, (const char *)pValue));
|
|
}
|
|
if (iSelect == 'keep')
|
|
{
|
|
// enforce a minimum keep-alive interval of 30s
|
|
if (iValue < 30)
|
|
{
|
|
NetPrintf(("protowebsocket: [%p] warning: enforcing a minimum keep-alive interval (%ds->30s)\n", pWebSocket, iValue));
|
|
iValue = 30;
|
|
}
|
|
pWebSocket->iKeepAlive = iValue*1000;
|
|
return(0);
|
|
}
|
|
if (iSelect == 'ires')
|
|
{
|
|
char *pInpBuf;
|
|
|
|
// make sure that we don't truncate any data already in the buffer, this assumes that you don't resize multiple times
|
|
iValue = DS_MAX(iValue, PROTOWEBSOCKET_DEFAULT_INPUTBUFSIZE);
|
|
|
|
// allocate buffer and reassign
|
|
if ((pInpBuf = (char *)DirtyMemAlloc(iValue, PROTOWEBSOCKET_MEMID, pWebSocket->iMemGroup, pWebSocket->pMemGroupUserData)) == NULL)
|
|
{
|
|
NetPrintf(("protowebsocket: [%p] unable to allocate buffer for resizing input buffer\n", pWebSocket));
|
|
return(-1);
|
|
}
|
|
ds_memcpy(pInpBuf, pWebSocket->pInpBuf, pWebSocket->iInpLen);
|
|
|
|
DirtyMemFree(pWebSocket->pInpBuf, PROTOWEBSOCKET_MEMID, pWebSocket->iMemGroup, pWebSocket->pMemGroupUserData);
|
|
pWebSocket->pInpBuf = pInpBuf;
|
|
pWebSocket->iInpMax = iValue;
|
|
|
|
return(0);
|
|
}
|
|
if ((iSelect == 'ping') || (iSelect == 'pong'))
|
|
{
|
|
return(_ProtoWebSocketSendPing(pWebSocket, (const char *)pValue, (iSelect == 'ping') ? PROTOWEBSOCKET_CTRL_PING : PROTOWEBSOCKET_CTRL_PONG));
|
|
}
|
|
if (iSelect == 'spam')
|
|
{
|
|
pWebSocket->iVerbose = iValue;
|
|
return(0);
|
|
}
|
|
if (iSelect == 'time')
|
|
{
|
|
pWebSocket->iTimeout = iValue*1000;
|
|
return(0);
|
|
}
|
|
// pass unhandled selectors through to ProtoSSL
|
|
return(ProtoSSLControl(pWebSocket->pProtoSSL, iSelect, iValue, iValue2, pValue));
|
|
}
|
|
|
|
/*F********************************************************************************/
|
|
/*!
|
|
\Function ProtoWebSocketUpdate
|
|
|
|
\Description
|
|
Give life to module (should be called periodically to allow module to
|
|
perform work).
|
|
|
|
\Input *pWebSocket - websocket module state
|
|
|
|
\Version 11/26/2012 (jbrookes)
|
|
*/
|
|
/********************************************************************************F*/
|
|
void ProtoWebSocketUpdate(ProtoWebSocketRefT *pWebSocket)
|
|
{
|
|
uint32_t uCurTick = NetTick();
|
|
int32_t iResult;
|
|
char *pStart, *pEnd;
|
|
|
|
// give time to comm module
|
|
ProtoSSLUpdate(pWebSocket->pProtoSSL);
|
|
|
|
// check for timeout
|
|
if ((pWebSocket->eState != ST_DISC) && (pWebSocket->eState != ST_FAIL))
|
|
{
|
|
if (NetTickDiff(uCurTick, pWebSocket->uTimer) > pWebSocket->iTimeout)
|
|
{
|
|
NetPrintf(("protowebsocket: [%p] inactivity timeout (state=%d)\n", pWebSocket, pWebSocket->eState));
|
|
pWebSocket->eState = ST_FAIL;
|
|
pWebSocket->bTimeout = TRUE;
|
|
}
|
|
}
|
|
|
|
// see if network connection is complete
|
|
if (pWebSocket->eState == ST_CONN)
|
|
{
|
|
if ((iResult = ProtoSSLStat(pWebSocket->pProtoSSL, 'stat', NULL, 0)) > 0)
|
|
{
|
|
pWebSocket->uTimer = uCurTick;
|
|
pWebSocket->eState = ST_SEND;
|
|
}
|
|
else if (iResult < 0)
|
|
{
|
|
_ProtoWebSocketFail(pWebSocket, iResult, "connecting");
|
|
}
|
|
}
|
|
|
|
// sending handshake
|
|
if (pWebSocket->eState == ST_SEND)
|
|
{
|
|
if ((iResult = _ProtoWebSocketSendBuff(pWebSocket)) > 0)
|
|
{
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 1, "protowebsocket: [%p] sent handshake request\n", pWebSocket));
|
|
#if DIRTYCODE_LOGGING
|
|
if (pWebSocket->iVerbose > 2)
|
|
{
|
|
NetPrintWrap(pWebSocket->pOutBuf, 80);
|
|
}
|
|
#endif
|
|
pWebSocket->eState = ST_RECV;
|
|
}
|
|
else if (iResult < 0)
|
|
{
|
|
_ProtoWebSocketFail(pWebSocket, iResult, "sending handshake request");
|
|
}
|
|
}
|
|
|
|
// receive server handshake response: note that due to serialized nature of handshaking
|
|
if (pWebSocket->eState == ST_RECV)
|
|
{
|
|
if ((iResult = _ProtoWebSocketRecv(pWebSocket, pWebSocket->pInpBuf+pWebSocket->iInpLen, pWebSocket->iInpMax-pWebSocket->iInpLen)) > 0)
|
|
{
|
|
pWebSocket->iInpLen += iResult;
|
|
// see if we have the complete header
|
|
pStart = pWebSocket->pInpBuf;
|
|
pEnd = pWebSocket->pInpBuf+pWebSocket->iInpLen-3;
|
|
|
|
// scan for blank line marking body start
|
|
while ((pStart != pEnd) && ((pStart[0] != '\r') || (pStart[1] != '\n') || (pStart[2] != '\r') || (pStart[3] != '\n')))
|
|
{
|
|
pStart += 1;
|
|
}
|
|
|
|
if (pStart != pEnd)
|
|
{
|
|
// process the header
|
|
if ((iResult = _ProtoWebSocketProcessHeader(pWebSocket, pWebSocket->pInpBuf)) >= 0)
|
|
{
|
|
NetPrintf(("protowebsocket: [%p] connection open\n", pWebSocket));
|
|
pWebSocket->eState = ST_OPEN;
|
|
}
|
|
else
|
|
{
|
|
_ProtoWebSocketFail(pWebSocket, iResult, "processing handshake response");
|
|
}
|
|
// remove header from buffer
|
|
pWebSocket->iInpOff = (int32_t)(pStart+4-pWebSocket->pInpBuf);
|
|
}
|
|
}
|
|
else if (iResult < 0)
|
|
{
|
|
_ProtoWebSocketFail(pWebSocket, iResult, "receiving handshake response");
|
|
}
|
|
}
|
|
|
|
// process in open state
|
|
if (pWebSocket->eState == ST_OPEN)
|
|
{
|
|
// issue a keep-alive?
|
|
if ((pWebSocket->iKeepAlive > 0) && (NetTickDiff(uCurTick, pWebSocket->uTimer) > pWebSocket->iKeepAlive))
|
|
{
|
|
NetPrintfVerbose((pWebSocket->iVerbose, 0, "protowebsocket: [%p] sending keep-alive at %u\n", pWebSocket, uCurTick));
|
|
pWebSocket->bDoPong = TRUE;
|
|
pWebSocket->strCtrlData[0] = '\0';
|
|
pWebSocket->iCtrlDataLen = 0;
|
|
}
|
|
|
|
// process pong request
|
|
_ProtoWebSocketProcessSendPong(pWebSocket);
|
|
|
|
// process close request
|
|
_ProtoWebSocketProcessSendClose(pWebSocket);
|
|
|
|
// process send request
|
|
_ProtoWebSocketProcessSend(pWebSocket);
|
|
}
|
|
}
|