Requests/Include/requests.mqh

574 lines
44 KiB
MQL5

//+------------------------------------------------------------------+
//| requests.mqh |
//| Copyright 2023, Omegafx |
//| https://www.mql5.com/en/users/omegajoctan/seller |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, Omegafx"
#property link "https://www.mql5.com/en/users/omegajoctan/seller"
//+------------------------------------------------------------------+
//| defines |
//+------------------------------------------------------------------+
#include <jason.mqh> //https://www.mql5.com/en/code/13663
#include <errordescription.mqh>
#include <Arrays\ArrayChar.mqh>
struct CResponse
{
int status_code; // HTTP status code (e.g., 200, 404)
string text; // Raw response body as string
CJAVal json; // Parses response as JSON
uchar content[]; // Raw bytes of the response
string headers; // Dictionary of response headers
string cookies; // Cookies set by the server
string url; // Final URL after redirects
bool ok; // True if status_code < 400
uint elapsed; // Time taken for the response in ms
string reason; // Text reason (e.g., "OK", "Not Found")
};
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
class CSession
{
protected:
static string WebStatusText(int code);
/**
* Base 64 is an encoding scheme that converts binary data into text
* format so that encoded textual data can be easily transported over
* network un-corrupted and without any data loss. Base64 is used
* commonly in a number of applications including email via MIME, and
* storing complex data in XML.
*/
static string Base64Encode(const string text)
{
uchar src[], dst[], key[];
StringToCharArray("", key, 0, StringLen(""));
StringToCharArray(text, src, 0, StringLen(text));
//--- encode src[] with BASE64
int res = CryptEncode(CRYPT_BASE64, src, key, dst);
return (res > 0) ? CharArrayToString(dst) : "";
}
static string Base64Decode(const string text)
{
uchar src[], dst[], key[];
StringToCharArray("", key, 0, StringLen(""));
StringToCharArray(text, src, 0, StringLen(text));
//--- decode src[] with BASE64
int res = CryptDecode(CRYPT_BASE64, src, key, dst);
return (res > 0) ? CharArrayToString(dst) : "";
}
static string StringTrim(string s)
{
StringTrimLeft(s);
StringTrimRight(s);
return s;
}
static string UpdateHeader(const string headers, const string key, const string value);
static string UpdateHeader(const string headers, const string new_header_key_value_pair);
static string GuessContentType(string filename);
static void CArray2Array(const CArrayChar &c_array, char &out_array[])
{
int size = c_array.Total();
ArrayResize(out_array, size);
for (int i=0; i<size; i++)
out_array[i] = c_array.At(i);
}
static string GetFileName(string base_filename)
{
string basename = base_filename;
int pos = StringFind(base_filename, "\\", StringLen(base_filename) - 1);
if (pos >= 0) basename = StringSubstr(base_filename, pos + 1);
pos = StringFind(basename, "/", StringLen(basename) - 1); // handle Unix-style paths
if (pos >= 0)
basename = StringSubstr(basename, pos + 1);
return basename;
}
static string URLEncode(const string value);
static string m_headers;
static string m_cookies;
public:
CSession(const string headers, const string cookies=""); // Provides headers cookies persistance
~CSession(void);
static void SetCookie(const string cookie)
{
if (StringLen(m_cookies) > 0)
m_cookies += "; ";
m_cookies += cookie;
}
static void ClearCookies() { m_cookies = ""; }
static void SetBasicAuth(const string username, const string password);
static string BuildUrlWithParams(string base_url, const string &keys[], const string &values[]);
//---
static CResponse request(const string method, const string url, const string data, const string &files[], const string headers = "", const int timeout = 5000, const bool is_json=true);
// High-level request helpers
static CResponse get(const string url, const string headers = "", const int timeout = 5000)
{
string files[];
return request("GET", url, "", files, headers, timeout, false);
}
static CResponse post(const string url, const string data, const string &files[], const string headers = "", const int timeout = 5000, const bool is_json=true)
{
return request("POST", url, data, files, headers, timeout, is_json);
}
static CResponse put(const string url, const string data, const string &files[], const string headers = "", const int timeout = 5000, const bool is_json=true)
{
return request("PUT", url, data, files, headers, timeout, is_json);
}
static CResponse patch(const string url, const string data = "", const string headers = "", const int timeout = 5000, const bool is_json=true)
{
string files[];
return request("PATCH", url, data, files, headers, timeout, is_json);
}
static CResponse delete_(const string url, const string headers = "", const int timeout = 5000, const bool is_json=true)
{
string files[];
return request("DELETE", url, "", files, headers, timeout, is_json);
}
};
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
// static members
string CSession::m_headers = "";
string CSession::m_cookies = "";
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CSession::CSession(const string headers, const string cookies="") // Provides headers and cookies persistance;
{
m_headers = headers;
m_cookies = cookies;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CSession::~CSession(void)
{
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CResponse CSession::request(const string method,
const string url,
const string data,
const string &files[],
const string headers = "",
const int timeout = 5000,
const bool is_json=true)
{
char result[];
string result_headers;
string temp_headers = m_headers;
string boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"; //for setting boundaries between data types and files in the form data
CArrayChar final_body; //Final body uchar array
CResponse response; //a structure containing various response fields
// Append user headers
if (headers != "")
temp_headers += headers + "\r\n";
bool use_multipart = ArraySize(files) > 0; //Check if files are attached
//--- Create a multi part request
if (use_multipart) // If multipart, assemble full body (JSON + files)
{
temp_headers = UpdateHeader(temp_headers, "Content-Type", "multipart/form-data; boundary=" + boundary + "\r\n"); //Update the headers
//--- JSON part (or form data)
if (StringLen(data) > 0)
{
string json_data = "";
if (is_json) //if Json data is given alongside the files
{
CJAVal js(NULL, jtUNDEF);
if (js.Deserialize(data, CP_UTF8))
js.Serialize(json_data); //Serialize the JSON data
}
string json_part = "--" + boundary + "\r\n";
json_part += "Content-Disposition: form-data; name=\"metadata\"\r\n";
json_part += "Content-Type: application/json\r\n\r\n";
json_part += json_data + "\r\n";
char json_bytes[];
StringToCharArray(json_part, json_bytes, 0, StringLen(json_part), CP_UTF8);
final_body.AddArray(json_bytes);
}
//--- File parts
for (uint i = 0; i < files.Size(); i++)
{
string filename = GetFileName(files[i]);
char file_data[]; //for storing the file data in binary format
int file_handle = FileOpen(filename, FILE_BIN | FILE_SHARE_READ); // Read the file in binary format
if (file_handle == INVALID_HANDLE)
{
printf("func=%s line=%d, Failed to read the file '%s'. Error = %s",__FUNCTION__,__LINE__,filename,ErrorDescription(GetLastError()));
continue; //skip to the next file if the current file is invalid
}
int fsize = (int)FileSize(file_handle);
ArrayResize(file_data, fsize);
if (FileReadArray(file_handle, file_data, 0, fsize)==0)
{
printf("func=%s line=%d, No data found in the file '%s'. Error = %s",__FUNCTION__,__LINE__,filename,ErrorDescription(GetLastError()));
FileClose(file_handle);
continue; //skip to the next file if the current file is invalid
}
FileClose(file_handle); //close the current file
//--- Append files header and content type as detected to the request
string file_part = "--" + boundary + "\r\n";
file_part += StringFormat("Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n", filename);
file_part += StringFormat("Content-Type: %s\r\n\r\n", GuessContentType(filename));
char file_header[];
StringToCharArray(file_part, file_header, 0, StringLen(file_part), CP_UTF8); //UTF-8 Encoding is a must
final_body.AddArray(file_header); //Add the file header
final_body.AddArray(file_data); //Add the file in binary format, the actual file
//--- append the new line — critical for HTTP form parsing.
final_body.Add('\r');
final_body.Add('\n');
}
//--- Final boundary
string closing = "--" + boundary + "--\r\n";
char closing_part[];
StringToCharArray(closing, closing_part);
final_body.AddArray(closing_part);
}
else // no files attached
{
//--- If it's just JSON or plain form data
string body_data = data;
if (is_json)
{
CJAVal js(NULL, jtUNDEF);
if (js.Deserialize(data, CP_UTF8))
js.Serialize(body_data);
temp_headers = UpdateHeader(temp_headers, "Content-Type", "application/json");
}
else
temp_headers = UpdateHeader(temp_headers, headers);
//---
char array[];
StringToCharArray(body_data, array, 0, StringLen(body_data), CP_UTF8); //Use UTF-8 similar requests in Python, This is very crucial
final_body.AddArray(array);
}
char final_body_char_arr[];
CArray2Array(final_body, final_body_char_arr);
if (MQLInfoInteger(MQL_DEBUG))
Print("Final body:\n",CharArrayToString(final_body_char_arr, 0 , final_body.Total(), CP_UTF8));
//--- Add cookies if there are any
if (StringLen(m_cookies) > 0)
temp_headers = UpdateHeader(temp_headers, "Cookie", m_cookies);
//--- Send the request
uint start = GetTickCount(); //starting time of the request
int status = WebRequest(method, url, temp_headers, timeout, final_body_char_arr, result, result_headers); //trigger a webrequest function
if(status == -1)
{
PrintFormat("WebRequest failed with error %s", ErrorDescription(GetLastError()));
response.status_code = 0;
return response;
}
//--- Fill the response struct
response.elapsed = GetTickCount() - start;
response.text = CharArrayToString(result);
response.status_code = status;
response.headers = result_headers;
response.url = url;
response.ok = (status >= 200 && status < 400);
response.reason = WebStatusText(status);
ArrayCopy(response.content, result);
//---
CJAVal js;
if (js.Deserialize(response.text))
response.json = js;
response.cookies = response.json[""]["cookies"].ToStr();
//---
return response;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
string CSession::WebStatusText(int code)
{
string reason = "";
switch(code)
{
case 200:
reason = "OK";
break;
case 201:
reason = "Created";
break;
case 400:
reason = "Bad Request";
break;
case 401:
reason = "Unauthorized";
break;
case 403:
reason = "Forbidden";
break;
case 404:
reason = "Not Found";
break;
case 500:
reason = "Internal Server Error";
break;
default:
reason = "HTTP " + IntegerToString(code);
break;
}
return reason;
}
//+------------------------------------------------------------------+
//| Sets or replaces default headers |
//+------------------------------------------------------------------+
string CSession::UpdateHeader(const string headers, const string key, const string value)
{
string res_headers = "";
if(StringFind(headers, key + ":") >= 0)
{
// Replace existing header
int start = StringFind(headers, key + ":");
int end = StringFind(headers, "\r\n", start);
if(end == -1)
end = StringLen(headers);
res_headers = StringSubstr(headers, 0, start) +
key + ": " + value + "\r\n" +
StringSubstr(headers, end + 2);
}
else
{
// Add new header
res_headers += key + ": " + value + "\r\n";
}
return res_headers;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
string CSession::UpdateHeader(const string headers, const string new_header_key_value_pair)
{
string key = "";
string value = "";
// Split the input string into key and value based on the first colon
int colon_index = StringFind(new_header_key_value_pair, ":");
if(colon_index > -1)
{
key = StringTrim(StringSubstr(new_header_key_value_pair, 0, colon_index));
value = StringTrim(StringSubstr(new_header_key_value_pair, colon_index + 1));
}
else
{
// Invalid format; return headers unmodified
return headers;
}
string res_headers = headers;
int start = StringFind(headers, key + ":");
if(start >= 0)
{
// Replace existing header
int end = StringFind(headers, "\r\n", start);
if(end == -1)
end = StringLen(headers);
res_headers = StringSubstr(headers, 0, start) +
key + ": " + value + "\r\n" +
StringSubstr(headers, end + 2);
}
else
{
// Add new header
res_headers += key + ": " + value + "\r\n";
}
return res_headers;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CSession::SetBasicAuth(const string username, const string password)
{
string credentials = username + ":" + password;
string encoded = Base64Encode(credentials); //Encode the credentials
m_headers = UpdateHeader(m_headers, "Authorization", "Basic " + encoded); //Update HTTP headers with the authentication information
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
string CSession::GuessContentType(string filename)
{
StringToLower(filename); // Normalize for case-insensitivity
if(StringFind(filename, ".txt") >= 0) return "text/plain";
if(StringFind(filename, ".json") >= 0) return "application/json";
if(StringFind(filename, ".xml") >= 0) return "application/xml";
if(StringFind(filename, ".csv") >= 0) return "text/csv";
if(StringFind(filename, ".html") >= 0) return "text/html";
if(StringFind(filename, ".htm") >= 0) return "text/html";
//--- Images
if(StringFind(filename, ".png") >= 0) return "image/png";
if(StringFind(filename, ".jpg") >= 0 || StringFind(filename, ".jpeg") >= 0) return "image/jpeg";
if(StringFind(filename, ".gif") >= 0) return "image/gif";
if(StringFind(filename, ".bmp") >= 0) return "image/bmp";
if(StringFind(filename, ".webp") >= 0) return "image/webp";
if(StringFind(filename, ".ico") >= 0) return "image/x-icon";
if(StringFind(filename, ".svg") >= 0) return "image/svg+xml";
//--- Audio
if(StringFind(filename, ".mp3") >= 0) return "audio/mpeg";
if(StringFind(filename, ".wav") >= 0) return "audio/wav";
if(StringFind(filename, ".ogg") >= 0) return "audio/ogg";
//--- Video
if(StringFind(filename, ".mp4") >= 0) return "video/mp4";
if(StringFind(filename, ".avi") >= 0) return "video/x-msvideo";
if(StringFind(filename, ".mov") >= 0) return "video/quicktime";
if(StringFind(filename, ".webm") >= 0) return "video/webm";
if(StringFind(filename, ".mkv") >= 0) return "video/x-matroska";
//--- Applications
if(StringFind(filename, ".pdf") >= 0) return "application/pdf";
if(StringFind(filename, ".zip") >= 0) return "application/zip";
if(StringFind(filename, ".gz") >= 0) return "application/gzip";
if(StringFind(filename, ".tar") >= 0) return "application/x-tar";
if(StringFind(filename, ".rar") >= 0) return "application/vnd.rar";
if(StringFind(filename, ".7z") >= 0) return "application/x-7z-compressed";
if(StringFind(filename, ".exe") >= 0) return "application/octet-stream";
if(StringFind(filename, ".apk") >= 0) return "application/vnd.android.package-archive";
//--- Microsoft Office
if(StringFind(filename, ".doc") >= 0) return "application/msword";
if(StringFind(filename, ".docx") >= 0) return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
if(StringFind(filename, ".xls") >= 0) return "application/vnd.ms-excel";
if(StringFind(filename, ".xlsx") >= 0) return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
if(StringFind(filename, ".ppt") >= 0) return "application/vnd.ms-powerpoint";
if(StringFind(filename, ".pptx") >= 0) return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
return "application/octet-stream"; // Default fallback
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
string CSession::URLEncode(const string value)
{
uchar bytes[];
StringToCharArray(value, bytes, 0, StringLen(value), CP_UTF8);
string encoded = "";
for (int i = 0; i < ArraySize(bytes); ++i)
{
uchar c = bytes[i];
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
c == '-' || c == '_' || c == '.' || c == '~')
{
encoded += CharToString(c);
}
else if (c == ' ')
{
encoded += "+";
}
else
{
encoded += StringFormat("%%%02X", c);
}
}
return encoded;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
string CSession::BuildUrlWithParams(string base_url, const string &keys[], const string &values[])
{
if (keys.Size() != values.Size())
{
printf("func=%s line=%d, Failed. Keys and values array sizes dimensional mismatch",__FUNCTION__,__LINE__);
return "";
}
//---
string query = "";
for (int i = 0; i < ArraySize(keys); ++i)
{
if (i > 0)
query += "&";
query += URLEncode(keys[i]) + "=" + URLEncode(values[i]);
}
return base_url + "?" + query;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+