forked from airat77786/MQL5Book
544 lines
16 KiB
MQL5
544 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;
| |||
}
| |||
};
| |||
//+------------------------------------------------------------------+
|