MQL5Book/Scripts/p7/SocketReadWriteHTTPS.mq5

318 lines
12 KiB
MQL5
Raw Permalink Normal View History

2025-05-30 16:09:41 +02:00
//+------------------------------------------------------------------+
//| 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>
*/
//+------------------------------------------------------------------+