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;
|
|
}
|
|
};
|
|
//+------------------------------------------------------------------+
|