545 lines
16 KiB
MQL5
545 lines
16 KiB
MQL5
|
//+------------------------------------------------------------------+
|
||
|
//| wsprotocol.mqh |
|
||
|
//| Copyright 2020-2022, MetaQuotes Ltd. |
|
||
|
//| https://www.mql5.com |
|
||
|
//+------------------------------------------------------------------+
|
||
|
#include "wstools.mqh"
|
||
|
#include "wsframe.mqh"
|
||
|
#include "wsmessage.mqh"
|
||
|
#include "wsinterfaces.mqh"
|
||
|
|
||
|
#define Hybi WebSocketConnectionHybi
|
||
|
#define Hixie WebSocketConnectionHixie
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| WebSocket protocol common part |
|
||
|
//+------------------------------------------------------------------+
|
||
|
class WebSocketConnection: public IWebSocketConnection
|
||
|
{
|
||
|
protected:
|
||
|
IWebSocketObserver *owner;
|
||
|
IWebSocketTransport *socket;
|
||
|
bool deflate;
|
||
|
|
||
|
bool disconnecting; // hard close at socket level
|
||
|
bool closeRequested; // soft close at protocol level
|
||
|
uchar writeBuffer[];
|
||
|
uchar tail[];
|
||
|
|
||
|
string headers[][2];
|
||
|
|
||
|
void adjustCompression()
|
||
|
{
|
||
|
for(int i = 0; i < ArrayRange(headers, 0); i++)
|
||
|
{
|
||
|
if(StringFind(headers[i][0], "sec-websocket-extensions") == 0
|
||
|
&& StringFind(headers[i][1], "permessage-deflate") >= 0)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
deflate = false;
|
||
|
Print("Deflate doesn't supported by server");
|
||
|
}
|
||
|
|
||
|
bool writeInternalBuffer()
|
||
|
{
|
||
|
uchar buff[];
|
||
|
const int n = ArraySize(writeBuffer);
|
||
|
if(n > WS_BUFFSIZE)
|
||
|
{
|
||
|
ArrayCopy(buff, writeBuffer, 0, 0, WS_BUFFSIZE);
|
||
|
ArrayCopy(writeBuffer, writeBuffer, 0, WS_BUFFSIZE);
|
||
|
ArrayResize(writeBuffer, n - WS_BUFFSIZE);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
ArrayCopy(buff, writeBuffer);
|
||
|
ArrayResize(writeBuffer, 0);
|
||
|
}
|
||
|
|
||
|
if(!writePacketToNet(buff))
|
||
|
{
|
||
|
close();
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if(ArraySize(writeBuffer) == 0 && disconnecting)
|
||
|
{
|
||
|
close();
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
int writePacketToNet(const uchar &data[])
|
||
|
{
|
||
|
const int n = ArraySize(data);
|
||
|
int fwrite = 0;
|
||
|
int written = 0;
|
||
|
uchar buffer[];
|
||
|
for(; written < n; written += fwrite)
|
||
|
{
|
||
|
if(written > 0)
|
||
|
{
|
||
|
ArrayResize(buffer, 0); // shrink to 0 if something left from previous iteration
|
||
|
ArrayCopy(buffer, data, 0, written);
|
||
|
fwrite = socket.write(buffer);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
fwrite = socket.write(data);
|
||
|
}
|
||
|
if(fwrite == -1)
|
||
|
{
|
||
|
Print("Write failed: ", _LastError);
|
||
|
return 0; // can't
|
||
|
}
|
||
|
}
|
||
|
return written;
|
||
|
}
|
||
|
|
||
|
int readPacketFromNet(uchar &data[], const bool waitForHeaders = false)
|
||
|
{
|
||
|
uchar buffer[];
|
||
|
int read = 0;
|
||
|
int cursor = 0;
|
||
|
|
||
|
do
|
||
|
{
|
||
|
read = socket.read(buffer);
|
||
|
if(read > 0)
|
||
|
{
|
||
|
ArrayCopy(data, buffer, cursor, 0, read);
|
||
|
cursor += read;
|
||
|
}
|
||
|
|
||
|
if(waitForHeaders)
|
||
|
{
|
||
|
if(ArraySize(data) > 4 && StringFind(CharArrayToString(data), CRLFCRLF) > 0)
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
while(((socket.isReadable() && read > 0)
|
||
|
|| (waitForHeaders && read != -1 && !_LastError)) && !IsStopped());
|
||
|
|
||
|
if(!socket.isConnected())
|
||
|
{
|
||
|
close(); // notify subscribers
|
||
|
}
|
||
|
|
||
|
return cursor;
|
||
|
}
|
||
|
|
||
|
bool write(const uchar &data[])
|
||
|
{
|
||
|
ArrayCopy(writeBuffer, data, ArraySize(writeBuffer));
|
||
|
|
||
|
while(ArraySize(writeBuffer) > 0)
|
||
|
{
|
||
|
if(!writeInternalBuffer()) return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
int bufferSwap(uchar &buff1[], uchar &buff2[])
|
||
|
{
|
||
|
return ArraySwap(buff1, buff2) ? fmax(ArraySize(buff1), ArraySize(buff2)) : 0;
|
||
|
}
|
||
|
|
||
|
void close()
|
||
|
{
|
||
|
if(!disconnecting)
|
||
|
{
|
||
|
disconnecting = true;
|
||
|
socket.close();
|
||
|
owner.onDisconnect();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
string serializeHeaders(const string custom = NULL)
|
||
|
{
|
||
|
string str = "";
|
||
|
|
||
|
for(int i = 0; i < ArrayRange(headers, 0); i++)
|
||
|
{
|
||
|
str += headers[i][0] + " " + headers[i][1] + "\r\n";
|
||
|
}
|
||
|
if(StringLen(custom)) str += custom;
|
||
|
|
||
|
str += "\r\n";
|
||
|
|
||
|
return str;
|
||
|
}
|
||
|
|
||
|
public:
|
||
|
WebSocketConnection(IWebSocketObserver *client, IWebSocketTransport *trans, const bool compression = false): deflate(compression)
|
||
|
{
|
||
|
owner = client;
|
||
|
socket = trans;
|
||
|
disconnecting = false;
|
||
|
closeRequested = false;
|
||
|
}
|
||
|
|
||
|
virtual bool handshake(const string url, const string host, const string origin, const string custom = NULL)
|
||
|
{
|
||
|
uchar buffer[];
|
||
|
WsTools::StringToByteArray(serializeHeaders(custom), buffer);
|
||
|
|
||
|
socket.write(buffer);
|
||
|
|
||
|
ArrayResize(buffer, 0); // shrink
|
||
|
|
||
|
// wait for response with headers
|
||
|
const int len = readPacketFromNet(buffer, true);
|
||
|
string response = CharArrayToString(buffer, 0, WHOLE_ARRAY, CP_UTF8);
|
||
|
PrintFormat("Buffer: '%s'", response);
|
||
|
if(len == 0) return false;
|
||
|
const int end = StringFind(response, CRLFCRLF);
|
||
|
if(ArraySize(buffer) > end + StringLen(CRLFCRLF))
|
||
|
{
|
||
|
ArrayCopy(tail, buffer, 0, end + StringLen(CRLFCRLF));
|
||
|
StringSetLength(response, end);
|
||
|
}
|
||
|
WsTools::parseHeaders(response, headers); // re-fill headers from responce
|
||
|
Print("Headers: ");
|
||
|
ArrayPrint(headers);
|
||
|
|
||
|
if(deflate) adjustCompression(); // look for "Sec-WebSocket-Extensions: permessage-deflate"
|
||
|
owner.onConnected();
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
bool sendFrame(IWebSocketFrame *frame) override
|
||
|
{
|
||
|
uchar encoded[];
|
||
|
frame.encode(encoded);
|
||
|
const bool sent = writePacketToNet(encoded);
|
||
|
if(sent && frame.getType() == WS_CLOSE_FRAME)
|
||
|
{
|
||
|
Print("Close requested");
|
||
|
closeRequested = true;
|
||
|
}
|
||
|
return sent;
|
||
|
}
|
||
|
|
||
|
bool sendMessage(IWebSocketMessage *msg) override
|
||
|
{
|
||
|
IWebSocketFrame *frames[];
|
||
|
msg.getFrames(frames);
|
||
|
for(int i = 0; i < ArraySize(frames); i++)
|
||
|
{
|
||
|
if(!sendFrame(frames[i])) return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
int checkMessages() override
|
||
|
{
|
||
|
IWebSocketFrame *dummy[1];
|
||
|
return readFrame(dummy);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| WebSocket protocol Hybi specific part |
|
||
|
//+------------------------------------------------------------------+
|
||
|
class WebSocketConnectionHybi: public WebSocketConnection
|
||
|
{
|
||
|
protected:
|
||
|
IWebSocketMessage *openMessage;
|
||
|
IWebSocketFrame *lastFrame;
|
||
|
|
||
|
// Process single frame: create a new message from it or append to existing message;
|
||
|
// when message is complete (all frames received), it's sent to onMessage() handler
|
||
|
void processMessageFrame(IWebSocketFrame *frame)
|
||
|
{
|
||
|
if(openMessage && !openMessage.isFinalised())
|
||
|
{
|
||
|
openMessage.takeFrame(frame);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
openMessage = new WebSocketMessage(frame.isCompressed());
|
||
|
openMessage.takeFrame(frame);
|
||
|
}
|
||
|
|
||
|
if(openMessage && openMessage.isFinalised())
|
||
|
{
|
||
|
owner.onMessage(openMessage);
|
||
|
openMessage = NULL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Handle incoming control frames: sends Pong on Ping and close connection after a Close request
|
||
|
void processControlFrame(IWebSocketFrame *frame)
|
||
|
{
|
||
|
switch(frame.getType())
|
||
|
{
|
||
|
case WS_FRAME_OPCODE::WS_CLOSE_FRAME:
|
||
|
if(closeRequested) // our close was confirmed
|
||
|
{
|
||
|
Print("Server close ack");
|
||
|
}
|
||
|
else if(!disconnecting) // server initiated close
|
||
|
{
|
||
|
if(openMessage) // still not finalized(!)
|
||
|
{
|
||
|
owner.onMessage(openMessage);
|
||
|
openMessage = NULL;
|
||
|
}
|
||
|
|
||
|
WebSocketFrame temp(WS_FRAME_OPCODE::WS_CLOSE_FRAME); // send our ack
|
||
|
sendFrame(&temp);
|
||
|
}
|
||
|
close();
|
||
|
break;
|
||
|
case WS_FRAME_OPCODE::WS_PING_FRAME:
|
||
|
{
|
||
|
IWebSocketFrame *temp = WebSocketFrame::create(WS_FRAME_OPCODE::WS_PONG_FRAME, frame.getData());
|
||
|
sendFrame(temp);
|
||
|
delete temp;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool send(WebSocketMessage *m)
|
||
|
{
|
||
|
return sendMessage(m) || disconnect(); // success status
|
||
|
}
|
||
|
|
||
|
public:
|
||
|
WebSocketConnectionHybi(IWebSocketObserver *client, IWebSocketTransport *trans,
|
||
|
const bool compression = false): WebSocketConnection(client, trans, compression)
|
||
|
{
|
||
|
openMessage = NULL;
|
||
|
lastFrame = NULL;
|
||
|
}
|
||
|
|
||
|
~WebSocketConnectionHybi()
|
||
|
{
|
||
|
if(CheckPointer(openMessage) == POINTER_DYNAMIC) delete openMessage; // incomplete message
|
||
|
if(CheckPointer(lastFrame) == POINTER_DYNAMIC) delete lastFrame; // orphan frame
|
||
|
}
|
||
|
|
||
|
static string randHybiKey()
|
||
|
{
|
||
|
uchar chars[16];
|
||
|
for(int i = 0; i < 16; i++)
|
||
|
{
|
||
|
chars[i] = (uchar)(rand() % 256);
|
||
|
}
|
||
|
|
||
|
uchar key[];
|
||
|
uchar encoded[];
|
||
|
CryptEncode(CRYPT_BASE64, chars, key, encoded);
|
||
|
return CharArrayToString(encoded);
|
||
|
}
|
||
|
|
||
|
bool handshake(const string url, const string host, const string origin, const string custom = NULL) override
|
||
|
{
|
||
|
const string handshakeChallenge = randHybiKey();
|
||
|
|
||
|
WsTools::push(headers, "GET", url + " HTTP/1.1");
|
||
|
WsTools::push(headers, "Connection:", "Upgrade");
|
||
|
WsTools::push(headers, "Host:", host);
|
||
|
WsTools::push(headers, "Sec-WebSocket-Key:", handshakeChallenge);
|
||
|
WsTools::push(headers, "Origin:", origin);
|
||
|
WsTools::push(headers, "Sec-WebSocket-Version:", (string)13);
|
||
|
WsTools::push(headers, "Upgrade:", "websocket");
|
||
|
if(deflate)
|
||
|
{
|
||
|
WsTools::push(headers, "Sec-WebSocket-Extensions:", "permessage-deflate; server_no_context_takeover; client_no_context_takeover; client_max_window_bits=15; server_max_window_bits=15");//client_max_window_bits; server_max_window_bits=15; client_max_window_bits=15
|
||
|
}
|
||
|
return WebSocketConnection::handshake(url, host, origin, custom);
|
||
|
}
|
||
|
|
||
|
int readFrame(IWebSocketFrame *&frames[]) override
|
||
|
{
|
||
|
uchar data[];
|
||
|
bool closed = false;
|
||
|
const int n = (ArraySize(tail) > 0) ? bufferSwap(tail, data) : readPacketFromNet(data);
|
||
|
while(ArraySize(data) > 0 && !closed)
|
||
|
{
|
||
|
IWebSocketFrame *frame = WebSocketFrame::decode(data, lastFrame);
|
||
|
if(frame)
|
||
|
{
|
||
|
if(frame.isReady())
|
||
|
{
|
||
|
if(frame.isControlFrame())
|
||
|
{
|
||
|
processControlFrame(frame);
|
||
|
closed = (frame.getType() == WS_FRAME_OPCODE::WS_CLOSE_FRAME);
|
||
|
delete frame;
|
||
|
frame = NULL;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
processMessageFrame(frame);
|
||
|
if(ArrayIsDynamic(frames))
|
||
|
{
|
||
|
WsTools::push(frames, frame);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
lastFrame = NULL;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
lastFrame = frame;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Print(__FUNCSIG__, " unexpected content - not a valid frame");
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
return ArraySize(frames);
|
||
|
}
|
||
|
|
||
|
bool sendString(const string msg) override
|
||
|
{
|
||
|
WebSocketMessage m(msg, deflate);
|
||
|
return send(&m);
|
||
|
}
|
||
|
|
||
|
bool sendData(const uchar &data[]) override
|
||
|
{
|
||
|
WebSocketMessage m(data, deflate);
|
||
|
return send(&m);
|
||
|
}
|
||
|
|
||
|
bool disconnect() override
|
||
|
{
|
||
|
WebSocketFrame f(WS_FRAME_OPCODE::WS_CLOSE_FRAME);
|
||
|
sendFrame(&f);
|
||
|
|
||
|
int i = 0;
|
||
|
do
|
||
|
{
|
||
|
i++;
|
||
|
Print("Waiting...");
|
||
|
Sleep(100);
|
||
|
checkMessages();
|
||
|
}
|
||
|
while(i < 5 && socket.isConnected());
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| WebSocket protocol Hixie specific part (not tested in MQL5!) |
|
||
|
//+------------------------------------------------------------------+
|
||
|
class WebSocketConnectionHixie: public WebSocketConnection
|
||
|
{
|
||
|
// In Hixie protocol, a message contains 1 frame always
|
||
|
public:
|
||
|
WebSocketConnectionHixie(IWebSocketObserver *client, IWebSocketTransport *trans,
|
||
|
const bool = false): WebSocketConnection(client, trans)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
struct HixieKey
|
||
|
{
|
||
|
long number;
|
||
|
string key;
|
||
|
};
|
||
|
|
||
|
static HixieKey randHixieKey()
|
||
|
{
|
||
|
int spaces_n = rand() % 12 + 1;
|
||
|
int max_n = INT_MAX / spaces_n;
|
||
|
int number_n = rand() % max_n;
|
||
|
long product_n = number_n * spaces_n;
|
||
|
string key_n = IntegerToString(product_n);
|
||
|
int range = rand() % 12 + 1;
|
||
|
for(int i = 0; i < range; i++)
|
||
|
{
|
||
|
uchar c;
|
||
|
if(rand() > 32767 / 2)
|
||
|
{
|
||
|
c = (uchar)(rand() % (0x2f + 1 - 0x21 + 1) + 0x21);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
c = (uchar)(rand() % (0x7e + 1 - 0x3a + 1) + 0x3a);
|
||
|
}
|
||
|
int len = StringLen(key_n);
|
||
|
int pos = rand() % len;
|
||
|
string key_n1 = StringSubstr(key_n, 0, pos);
|
||
|
string key_n2 = StringSubstr(key_n, pos);
|
||
|
key_n = key_n1 + (string)c + key_n2;
|
||
|
}
|
||
|
|
||
|
for(int i = 0; i < spaces_n; i++)
|
||
|
{
|
||
|
int len = StringLen(key_n);
|
||
|
int pos = rand() % (len - 1) + 1;
|
||
|
string key_n1 = StringSubstr(key_n, 0, pos);
|
||
|
string key_n2 = StringSubstr(key_n, pos);
|
||
|
key_n = key_n1 + " " + key_n2;
|
||
|
}
|
||
|
HixieKey result;
|
||
|
result.number = number_n;
|
||
|
result.key = key_n;
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
bool handshake(const string url, const string host, const string origin, const string custom = NULL) override
|
||
|
{
|
||
|
const string hixieKey1 = randHixieKey().key;
|
||
|
const string hixieKey2 = randHixieKey().key;
|
||
|
WsTools::push(headers, "GET", url + " HTTP/1.1");
|
||
|
WsTools::push(headers, "Connection:", "Upgrade");
|
||
|
WsTools::push(headers, "Host:", host);
|
||
|
WsTools::push(headers, "Origin:", origin);
|
||
|
WsTools::push(headers, "Sec-WebSocket-Key1:", hixieKey1);
|
||
|
WsTools::push(headers, "Sec-WebSocket-Key2:", hixieKey2);
|
||
|
WsTools::push(headers, "Upgrade:", "websocket");
|
||
|
return WebSocketConnection::handshake(url, host, origin, custom);
|
||
|
}
|
||
|
|
||
|
int readFrame(IWebSocketFrame *&frames[]) override
|
||
|
{
|
||
|
uchar data[];
|
||
|
const int n = (ArraySize(tail) > 0) ? bufferSwap(tail, data) : readPacketFromNet(data);
|
||
|
IWebSocketFrame *f = WebSocketFrameHixie::decode(data);
|
||
|
if(f)
|
||
|
{
|
||
|
IWebSocketMessage *m = new WebSocketMessageHixie(f);
|
||
|
if(m)
|
||
|
{
|
||
|
WsTools::push(frames, f);
|
||
|
owner.onMessage(m); // user code is responsible for message object from now on
|
||
|
return 1;
|
||
|
}
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
bool sendString(const string msg) override
|
||
|
{
|
||
|
WebSocketMessageHixie m(msg);
|
||
|
return sendMessage(&m);
|
||
|
}
|
||
|
|
||
|
bool sendData(const uchar &data[]) override
|
||
|
{
|
||
|
return false; // not supported
|
||
|
}
|
||
|
|
||
|
bool disconnect() override
|
||
|
{
|
||
|
WebSocketFrameHixie f(WS_FRAME_OPCODE::WS_CLOSE_FRAME);
|
||
|
sendFrame(&f);
|
||
|
close();
|
||
|
return false;
|
||
|
}
|
||
|
};
|
||
|
//+------------------------------------------------------------------+
|