forked from airat77786/MQL5Book
317 lines
12 KiB
MQL5
317 lines
12 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| SocketReadWriteHTTPS.mq5 |
| |||
//| Copyright 2022, MetaQuotes Ltd. |
| |||
//| https://www.mql5.com |
| |||
//+------------------------------------------------------------------+
| |||
#property description "Executes complete HTTP-request with given method, address, and port. Secure connections on port 443 are supported."
| |||
#property description "NB: Default 'Server' requires to allow 'www.google.com' in terminal settings - to use other servers, change the settings accordingly."
| |||
#property script_show_inputs
| |||
| |||
#include "..\..\Include\PRTF.mqh"
| |||
| |||
input string Method = "GET"; // Method (HEAD,GET)
| |||
input string Server = "www.google.com";
| |||
input uint Port = 443;
| |||
input uint Timeout = 5000;
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Send HTTP-request via socket (secure or insecure connection) |
| |||
//+------------------------------------------------------------------+
| |||
bool HTTPSend(int socket, const string request, const bool TLS)
| |||
{
| |||
char req[];
| |||
int len = StringToCharArray(request, req, 0, WHOLE_ARRAY, CP_UTF8) - 1;
| |||
if(len < 0) return false;
| |||
return (TLS ? SocketTlsSend(socket, req, len) : SocketSend(socket, req, len)) == len;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Helper converter hex string to number |
| |||
//+------------------------------------------------------------------+
| |||
int HexStringToInteger(string s, const int offset = 0)
| |||
{
| |||
int result = 0;
| |||
StringToUpper(s);
| |||
for(int i = offset; i < StringLen(s); ++i)
| |||
{
| |||
int code = s[i] - '0';
| |||
if(code > 9) code -= 'A' - ':';
| |||
if(code < 0 || code > 15) break; // unsupported char
| |||
result = result * 16 + code;
| |||
}
| |||
return result;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Pseudo-nonblocking probe and read of some data from socket |
| |||
//+------------------------------------------------------------------+
| |||
int SocketReadAvailable(int socket, uchar &block[], const uint maxlen = INT_MAX)
| |||
{
| |||
ArrayResize(block, 0);
| |||
const uint len = SocketIsReadable(socket);
| |||
if(len > 0)
| |||
return SocketRead(socket, block, fmin(len, maxlen), 10);
| |||
return 0;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Receive web-page via socket (secure or insecure connection) |
| |||
//+------------------------------------------------------------------+
| |||
bool HTTPRecv(int socket, string &result, const uint timeout, const bool TLS)
| |||
{
| |||
uchar response[]; // entire data (headers + document body)
| |||
uchar block[]; // single read block
| |||
int len; // current read block size (signed int to keep -1 in case of error)
| |||
int lastLF = -1; // position of LF(Line-Feed) symbol latest found
| |||
int body = 0; // offset where document body starts
| |||
int size = 0; // document size according to header
| |||
int chunk_size = 0, chunk_start = 0, chunk_n = 1;
| |||
const static string content_length = "Content-Length:";
| |||
const static string crlf = "\r\n";
| |||
const static int crlf_length = 2;
| |||
| |||
uint start = GetTickCount();
| |||
result = "";
| |||
| |||
do
| |||
{
| |||
ResetLastError();
| |||
if((len = (TLS ? SocketTlsReadAvailable(socket, block, 1024) :
| |||
SocketReadAvailable(socket, block, 1024))) > 0)
| |||
{
| |||
const int n = ArraySize(response);
| |||
ArrayCopy(response, block, n); // combine all received blocks
| |||
| |||
if(body == 0) // seach for header termination sequence until found
| |||
{
| |||
for(int i = n; i < ArraySize(response); ++i)
| |||
{
| |||
if(response[i] == '\n')
| |||
{
| |||
if(lastLF == i - crlf_length) // found "\r\n\r\n" sequence
| |||
{
| |||
body = i + 1;
| |||
string headers = CharArrayToString(response, 0, i);
| |||
Print("* HTTP-header found, header size: ", body);
| |||
Print(headers);
| |||
const int p = StringFind(headers, content_length); // TODO: should be case-insensitive!
| |||
if(p > -1)
| |||
{
| |||
size = (int)StringToInteger(StringSubstr(headers, p + StringLen(content_length)));
| |||
Print("* ", content_length, size);
| |||
}
| |||
else
| |||
{
| |||
size = -1; // server didn't report document length
| |||
// try to find chunk size in front of document
| |||
if(StringFind(headers, "Transfer-Encoding: chunked") > 0) // TODO: case-insensitive
| |||
{
| |||
// chunks syntax:
| |||
// hex-size\r\ncontent\r\n...
| |||
const string preview = CharArrayToString(response, body, 20);
| |||
chunk_size = HexStringToInteger(preview);
| |||
if(chunk_size > 0)
| |||
{
| |||
const int d = StringFind(preview, crlf) + crlf_length;
| |||
chunk_start = body;
| |||
Print("Chunk: ", chunk_size, " start at ", chunk_start, " -", d);
| |||
ArrayRemove(response, body, d);
| |||
}
| |||
}
| |||
}
| |||
break; // header/body boundary found
| |||
}
| |||
lastLF = i;
| |||
}
| |||
}
| |||
}
| |||
| |||
if(size == ArraySize(response) - body) // complete document
| |||
{
| |||
Print("* Complete document");
| |||
break;
| |||
}
| |||
else if(chunk_size > 0 && ArraySize(response) - chunk_start >= chunk_size)
| |||
{
| |||
Print("* ", chunk_n, " chunk done: ", chunk_size, " total: ", ArraySize(response));
| |||
const int p = chunk_start + chunk_size;
| |||
const string preview = CharArrayToString(response, p, 20);
| |||
if(StringLen(preview) > crlf_length
| |||
&& StringFind(preview, crlf, crlf_length) > crlf_length)
| |||
{
| |||
chunk_size = HexStringToInteger(preview, crlf_length);
| |||
if(chunk_size > 0)
| |||
{
| |||
int d = StringFind(preview, crlf, crlf_length) + crlf_length; // twice '\r\n'
| |||
chunk_start = p;
| |||
Print("Chunk: ", chunk_size, " start at ", chunk_start, " -", d);
| |||
ArrayRemove(response, chunk_start, d);
| |||
++chunk_n;
| |||
}
| |||
else
| |||
{
| |||
Print("* Final chunk");
| |||
ArrayRemove(response, p, 5); // "\r\n0\r\n"
| |||
break;
| |||
}
| |||
} // else wait for more data
| |||
}
| |||
start = GetTickCount();
| |||
}
| |||
else
| |||
{
| |||
if(len == 0) Sleep(10); // give more time for data to arrive
| |||
}
| |||
}
| |||
while(GetTickCount() - start < timeout && !IsStopped() && !_LastError);
| |||
| |||
if(_LastError) PRTF(_LastError);
| |||
| |||
if(ArraySize(response) > 0)
| |||
{
| |||
if(body != 0)
| |||
{
| |||
// TODO: we should check 'Content-Type:' for 'charset=UTF-8'
| |||
result = CharArrayToString(response, body, WHOLE_ARRAY, CP_UTF8);
| |||
}
| |||
else
| |||
{
| |||
// provide incomplete header for troubleshooting
| |||
result = CharArrayToString(response);
| |||
}
| |||
}
| |||
| |||
return StringLen(result) > 0;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Script program start function |
| |||
//+------------------------------------------------------------------+
| |||
void OnStart()
| |||
{
| |||
PRTF(Server);
| |||
PRTF(Port);
| |||
const int socket = PRTF(SocketCreate());
| |||
if(socket == INVALID_HANDLE) return;
| |||
SocketTimeouts(socket, Timeout, Timeout);
| |||
if(PRTF(SocketConnect(socket, Server, Port, Timeout)))
| |||
{
| |||
string subject, issuer, serial, thumbprint;
| |||
datetime expiration;
| |||
bool TLS = false;
| |||
if(PRTF(SocketTlsCertificate(socket, subject, issuer, serial, thumbprint, expiration)))
| |||
{
| |||
PRTF(subject);
| |||
PRTF(issuer);
| |||
PRTF(serial);
| |||
PRTF(thumbprint);
| |||
PRTF(expiration);
| |||
TLS = true;
| |||
}
| |||
else
| |||
{
| |||
ResetLastError(); // clear NETSOCKET_NO_CERTIFICATE(5275), continue in insecure mode
| |||
}
| |||
| |||
if(PRTF(HTTPSend(socket, StringFormat(
| |||
"%s / HTTP/1.1\r\nHost: %s\r\nUser-Agent: MetaTrader 5\r\n\r\n",
| |||
Method, Server), TLS)))
| |||
{
| |||
string response;
| |||
if(PRTF(HTTPRecv(socket, response, Timeout, TLS)))
| |||
{
| |||
Print("Got ", StringLen(response), " bytes");
| |||
// can be a big content, consider to save it into a file
| |||
if(StringLen(response) > 1000)
| |||
{
| |||
int h = FileOpen(Server + ".htm", FILE_WRITE | FILE_TXT | FILE_ANSI, 0, CP_UTF8);
| |||
FileWriteString(h, response);
| |||
FileClose(h);
| |||
}
| |||
else
| |||
{
| |||
Print(response);
| |||
}
| |||
}
| |||
}
| |||
}
| |||
SocketClose(socket);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
/*
| |||
Examples:
| |||
| |||
Server=www.google.com / ok
| |||
Port=443 / ok
| |||
SocketCreate()=1 / ok
| |||
SocketConnect(socket,Server,Port,Timeout)=true / ok
| |||
SocketTlsCertificate(socket,subject,issuer,serial,thumbprint,expiration)=true / ok
| |||
subject=CN=www.google.com / ok
| |||
issuer=C=US, O=Google Trust Services LLC, CN=GTS CA 1C3 / ok
| |||
serial=00c9c57583d70aa05d12161cde9ee32578 / ok
| |||
thumbprint=1EEE9A574CC92773EF948B50E79703F1B55556BF / ok
| |||
expiration=2022.10.03 08:25:10 / ok
| |||
HTTPSend(socket,StringFormat(%s / HTTP/1.1
| |||
Host: %s
| |||
| |||
,Method,Server),TLS)=true / ok
| |||
* HTTP-header found, header size: 1080
| |||
HTTP/1.1 200 OK
| |||
Date: Mon, 01 Aug 2022 20:48:35 GMT
| |||
Expires: -1
| |||
Cache-Control: private, max-age=0
| |||
Content-Type: text/html; charset=ISO-8859-1
| |||
P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info."
| |||
Server: gws
| |||
X-XSS-Protection: 0
| |||
X-Frame-Options: SAMEORIGIN
| |||
Set-Cookie: 1P_JAR=2022-08-01-20; expires=Wed, 31-Aug-2022 20:48:35 GMT; path=/; domain=.google.com; Secure
| |||
...
| |||
Accept-Ranges: none
| |||
Vary: Accept-Encoding
| |||
Transfer-Encoding: chunked
| |||
| |||
Chunk: 22172 start at 1080 -6
| |||
* 1 chunk done: 22172 total: 24081
| |||
Chunk: 30824 start at 23252 -8
| |||
* 2 chunk done: 30824 total: 54083
| |||
* Final chunk
| |||
HTTPRecv(socket,response,Timeout,TLS)=true / ok
| |||
Got 52998 bytes
| |||
| |||
| |||
| |||
Server=www.mql5.com / ok
| |||
Port=80 / ok
| |||
SocketCreate()=1 / ok
| |||
SocketConnect(socket,Server,Port,Timeout)=true / ok
| |||
SocketTlsCertificate(socket,subject,issuer,serial,thumbprint,expiration)=false / NETSOCKET_NO_CERTIFICATE(5275)
| |||
HTTPSend(socket,StringFormat(%s / HTTP/1.1
| |||
Host: %s
| |||
| |||
,Method,Server),TLS)=true / ok
| |||
* HTTP-header found, header size: 291
| |||
HTTP/1.1 301 Moved Permanently
| |||
Server: nginx
| |||
Date: Sun, 31 Jul 2022 19:28:57 GMT
| |||
Content-Type: text/html
| |||
Content-Length: 162
| |||
Connection: keep-alive
| |||
Location: https://www.mql5.com/
| |||
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
| |||
X-Frame-Options: SAMEORIGIN
| |||
| |||
* Content-Length:162
| |||
* Complete document
| |||
HTTPRecv(socket,response,Timeout,TLS)=true / ok
| |||
<html>
| |||
<head><title>301 Moved Permanently</title></head>
| |||
<body>
| |||
<center><h1>301 Moved Permanently</h1></center>
| |||
<hr><center>nginx</center>
| |||
</body>
| |||
</html>
| |||
| |||
*/
| |||
//+------------------------------------------------------------------+
|