Kawe Mazidjatari b3a68ed095 Add EABase, EAThread and DirtySDK to R5sdk
DirtySDK (EA's Dirty Sockets library) will be used for the LiveAPI implementation, and depends on: EABase, EAThread.
2024-04-05 18:29:03 +02:00

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);
}
}