//+------------------------------------------------------------------+ //| 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 //https://www.mql5.com/en/code/13663 #include #include 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= 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; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+