forked from rosh/MQL5Book
402 lines
12 KiB
MQL5
402 lines
12 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| 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;
| |||
}
| |||
};
| |||
//+------------------------------------------------------------------+
|