MQL5Book/Include/ws/wsframe.mqh

403 lines
12 KiB
MQL5
Raw Permalink Normal View History

2025-05-30 16:09:41 +02:00
//+------------------------------------------------------------------+
//| wsframe.mqh |
//| Copyright 2020-2022, MetaQuotes Ltd. |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#include "wstools.mqh"
#include "wsinterfaces.mqh"
//+------------------------------------------------------------------+
//| WebSocket frame according to Hybi RFC (current standard) |
//+------------------------------------------------------------------+
class WebSocketFrame: public IWebSocketFrame
{
protected:
// first byte
uchar FIN;
uchar RSV1;
uchar RSV2;
uchar RSV3;
WS_FRAME_OPCODE opcode;
// second byte
uchar mask;
uint payloadLength; // NB: this lib supports 32 bit length only
// if mask is enabled
uint maskingKey;
uchar payloadData[];
uint actualLength;
/*
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
*/
void setType(WS_FRAME_OPCODE type)
{
opcode = type;
if(type == WS_FRAME_OPCODE::WS_CLOSE_FRAME) // needed to make frame larger than 1 byte
{
mask = 1;
}
}
static bool IsBitSet(uchar byte, int pos)
{
return (byte & (1 << pos)) > 0;
}
static void rotMask(uchar &data[], uint key, int offset = 0)
{
WsTools::BYTES4 u4b(key);
for(int i = 0; i < ArraySize(data); i++)
{
int j = (i + offset) % 4;
data[i] = (data[i] ^ u4b[j]);
}
}
class Creator1: public IWebSocketFrame::Creator<WebSocketFrame>
{
};
virtual IWebSocketFrame::StaticCreator *getCreator() override
{
static Creator1 c1;
return &c1;
}
void assign(WS_FRAME_OPCODE type, const bool fin, const bool deflate, const int n)
{
setType(type);
FIN = fin; // TODO: adjust this for continuation frames
RSV1 = deflate; // NB: this can be set in the first frame only
payloadLength = n;
}
public:
WebSocketFrame(WS_FRAME_OPCODE deftype = WS_FRAME_OPCODE::WS_TEXT_FRAME)
{
setType(deftype);
FIN = true;
RSV1 = 0;
RSV2 = 0;
RSV3 = 0;
mask = WS_APP_TYPE;
payloadLength = 0;
maskingKey = 0;
actualLength = 0;
}
~WebSocketFrame()
{
}
static IWebSocketFrame *create(WS_FRAME_OPCODE type, const string data = NULL, const bool deflate = false)
{
WebSocketFrame *f = new WebSocketFrame();
const int n = WsTools::StringToByteArray(data, f.payloadData, 0, WHOLE_ARRAY,
type == WS_FRAME_OPCODE::WS_TEXT_FRAME && !deflate ? CP_UTF8 : CP_ACP);
f.assign(type, true, deflate, n);
return f;
}
static IWebSocketFrame *create(WS_FRAME_OPCODE type, const uchar &data[], const bool deflate = false)
{
WebSocketFrame *f = new WebSocketFrame();
const int n = ArrayCopy(f.payloadData, data, 0, 0);
f.assign(type, true, deflate, n);
return f;
}
bool isMasked() override
{
return mask == 1;
}
bool isCompressed() override
{
return RSV1 == 1;
}
WS_FRAME_OPCODE getType() override
{
return opcode;
}
int encode(uchar &encoded[]) override
{
payloadLength = ArraySize(payloadData);
uchar firstByte = (uchar)opcode;
uchar secondByte = 0;
firstByte += FIN * 128 + RSV1 * 64 + RSV2 * 32 + RSV3 * 16;
ArrayResize(encoded, 14); // allocate memory for maximal header
encoded[0] = firstByte;
int filled = 0;
if(payloadLength <= 125)
{
secondByte = (uchar)payloadLength;
secondByte += mask * 128;
encoded[1] = secondByte;
filled = 2;
}
else if(payloadLength <= 255 * 255 - 1)
{
secondByte = 126;
secondByte += mask * 128;
encoded[1] = secondByte;
WsTools::pack2((ushort)payloadLength, encoded, 2); // 16 bit, big endian byte order
filled = 4;
}
else
{
// TODO: max length is now 32 bits instead of 64
secondByte = 127;
secondByte += mask * 128;
encoded[1] = secondByte;
WsTools::pack4(0, encoded, 2);
WsTools::pack4(payloadLength, encoded, 6);
filled = 10;
}
uint key = 0;
if(mask)
{
key = ((uint)rand() | ((uint)rand() << 16)) % (4228250625); // rand is 2 bytes wide
WsTools::pack4(key, encoded, filled);
filled += 4;
key = MathSwap(key);
}
ArrayResize(encoded, filled + ArraySize(payloadData));
if(ArraySize(payloadData))
{
if(mask == 1) rotMask(payloadData, key);
ArrayCopy(encoded, payloadData, filled);
}
return ArraySize(encoded);
}
static IWebSocketFrame *decode(uchar &data[], IWebSocketFrame *head = NULL)
{
uint remaining = 0;
uint consumed = 0;
WebSocketFrame *frame = NULL;
if(head != NULL)
{
frame = head;
remaining = ArraySize(data);
}
else
{
if(ArraySize(data) < 2)
{
Print("Insufficient data for frame ", ArraySize(data));
return NULL;
}
frame = new WebSocketFrame();
// read the first two bytes, then chop them off
uchar firstByte = (uchar)data[0];
uchar secondByte = (uchar)data[1];
const uint n = ArraySize(data);
frame.FIN = IsBitSet(firstByte, 7);
frame.RSV1 = IsBitSet(firstByte, 6);
frame.RSV2 = IsBitSet(firstByte, 5);
frame.RSV3 = IsBitSet(firstByte, 4);
frame.mask = IsBitSet(secondByte, 7);
frame.opcode = (WS_FRAME_OPCODE)(firstByte & 0x0F);
if(!(frame.opcode >= WS_CONTINUATION_FRAME && frame.opcode <= WS_PONG_FRAME))
{
Print("Bad frame opcode");
}
int len = secondByte & ~128;
if(len <= 125)
{
frame.payloadLength = len;
consumed = 2;
}
else if(len == 126)
{
frame.payloadLength = WsTools::unpack2(data, 2);
consumed = 4;
}
else if(len == 127)
{
uint h = WsTools::unpack4(data, 2);
uint l = WsTools::unpack4(data, 6);
frame.payloadLength = (uint)(l + (h * 0x0100000000));
consumed = 10;
}
if(frame.mask)
{
WsTools::BYTES4 u4b(data, consumed);
frame.maskingKey = u4b.num;
consumed += 4;
}
remaining = n - consumed;
}
uint currentOffset = frame.actualLength;
uint fullLength = MathMin(frame.payloadLength - frame.actualLength, remaining);
frame.actualLength += fullLength;
uchar frameData[];
if(fullLength < remaining) // data contains current frame and beginning of the next frame
{
ArrayCopy(frameData, data, 0, consumed, fullLength);
ArrayCopy(data, data, 0, consumed + fullLength);
ArrayResize(data, remaining - fullLength);
}
else
{
ArrayCopy(frameData, data, 0, consumed);
ArrayResize(data, 0);
}
if(frame.mask)
{
rotMask(frameData, frame.maskingKey, currentOffset);
}
ArrayCopy(frame.payloadData, frameData, ArraySize(frame.payloadData));
return frame;
}
bool isReady()
{
if(actualLength > payloadLength)
{
Print("WebSocket frame size mismatch");
return true;
}
return (actualLength == payloadLength);
}
bool isFinal() override
{
return FIN == 1;
}
string getData() override
{
return CharArrayToString(payloadData, 0, WHOLE_ARRAY, CP_UTF8);
}
int getData(uchar &buf[]) override
{
return ArrayCopy(buf, payloadData);
}
};
//+------------------------------------------------------------------+
//| WebSocket frame according to Hixie RFC (back compatibility) |
//+------------------------------------------------------------------+
class WebSocketFrameHixie: public IWebSocketFrame
{
protected:
WS_FRAME_OPCODE opcode;
string payloadData;
public:
class Creator1: public IWebSocketFrame::Creator<WebSocketFrameHixie>
{
};
virtual IWebSocketFrame::StaticCreator *getCreator() override
{
static Creator1 c1;
return &c1;
}
WebSocketFrameHixie(WS_FRAME_OPCODE deftype = WS_FRAME_OPCODE::WS_TEXT_FRAME): opcode(deftype) { }
static WebSocketFrameHixie *create(WS_FRAME_OPCODE type, const string data = NULL, const bool deflate = false)
{
WebSocketFrameHixie *o = new WebSocketFrameHixie();
o.payloadData = data;
return o;
}
static WebSocketFrameHixie *create(WS_FRAME_OPCODE type, const uchar &data[], const bool deflate = false)
{
return NULL; // binary data not supported by design
}
int encode(uchar &encoded[]) override
{
if(opcode == WS_FRAME_OPCODE::WS_CLOSE_FRAME)
{
static const uchar closing[] = {0xFF, 0x00};
return ArrayCopy(encoded, closing);
}
return WsTools::StringToByteArray(CharToString(0) + payloadData + CharToString(255), encoded);
}
string getData() override
{
return payloadData;
}
int getData(uchar &buf[]) override
{
return StringToCharArray(payloadData, buf, 0, WHOLE_ARRAY, CP_UTF8);
}
WS_FRAME_OPCODE getType() override
{
return opcode;
}
static WebSocketFrameHixie *decode(uchar &str[], WebSocketFrameHixie *head = NULL)
{
if(ArraySize(str) < 2) return NULL;
if(str[0] == 0 && str[ArraySize(str) - 1] == 255) // NB: frames with 0x01+ first byte not supported here
{
WebSocketFrameHixie *o = new WebSocketFrameHixie();
o.payloadData = CharArrayToString(str, 1, ArraySize(str) - 2, CP_UTF8);
return o;
}
// NB: frames with length prefix (0x80+) not supported here
return NULL;
}
};
//+------------------------------------------------------------------+