Веб-проекты (Часть III): Система авторизации Laravel/MetaTrader 5

Веб-проекты (Часть III): Система авторизации Laravel/MetaTrader 5

MetaTrader 5Примеры | 21 февраля 2022, 10:30
2 881 10
Anatoli Kazharski
Anatoli Kazharski

Содержание


1. Введение

Продолжаем работать над системой авторизации в схеме из трёх составных частей проекта:

  • Серверная часть основана на фреймворке Laravel
  • Клиентская браузерная часть разрабатывается на фреймворке Nuxt.
  • Клиентская часть в торговом терминале MetaTrader 5

В предыдущей главе этой части (Веб-проекты (Часть II): Система авторизации Laravel/Nuxt) была представлена реализация для авторизации пользователей в браузерном веб-приложении. А в этот раз создадим подобную систему в торговом терминале MetaTrader 5 на чистом MQL5. Пользователи приложения смогут зарегистрироваться в системе, предоставив свои учётные данные, чтобы впоследствии можно было авторизоваться и получить доступ, к каким-нибудь данным, которые хранятся в серверной части приложения.

Темы, которые будут затронуты в этой статье: 

  • Работа с локальной базой данных (SQLite)
  • HTTP-запросы в MetaTrader 5
  • JSON-объекты в MQL5
  • Библиотека для работы с регулярными выражениями
  • Анализатор сетевых протоколов Wireshark
  • Графический интерфейс


2. Графический интерфейс

Для создания графического интерфейса воспользуемся библиотекой EasyAndFastGUI. При использовании этой библиотеки нужно придерживаться определённых правил, чтобы всё работало, как задумано. Очень кратко рассмотрим общую структуру приложения при использовании этой библиотеки и максимально быстрый способ создания графического интерфейса, чтобы Вам не приходилось обращаться сейчас к статьям, где всё описывалось более подробно. 

Для удобства создайте в папке MQL-проекта файл Program.mqh с классом программы CProgram. Класс CProgram должен наследоваться от класса CWndCreate, чтобы получить доступ ко всем свойствам и методам библиотеки для создания графических интерфейсов. Установите вызов метода для удаления всех объектов при деинициализации приложения и вызов метода таймера, чтобы во время работы приложения обновления объектов проходили корректно. 

#include <EasyAndFastGUI\WndCreate.mqh>

class CProgram : public CWndCreate {
 public:
                     CProgram(void);
                    ~CProgram(void);

   bool              OnInitEvent(void);
   void              OnDeinitEvent(const int reason);
   void              OnTimerEvent(void);

 protected:
   virtual void      OnEvent(const int id, const long &lparam, 
                              const double &dparam, const string &sparam);
};

CProgram::CProgram(void) {}
CProgram::~CProgram(void) {}

bool CProgram::OnInitEvent(void) {
   return true;
}

void CProgram::OnDeinitEvent(const int reason) {
   CWndCreate::Destroy();
}

void CProgram::OnTimerEvent(void) {
   CWndCreate::OnTimerEvent();
}

void CProgram::OnEvent(const int id,const long &lparam,
                        const double &dparam,const string &sparam)
{

}

Осталось только связать эти методы обработки событий с аналогичными функциями в главном файле MQL-приложения. Подключите файл Program.mqh, объявите экземпляр класса CProgram и затем сделайте так, как показано в листинге кода ниже:

#include "Program.mqh"
CProgram program;

int OnInit(void) {
   if(!program.OnInitEvent()) {
      return(INIT_FAILED);
   }
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason) {
   program.OnDeinitEvent(reason);
}

void OnTimer(void) {
   program.OnTimerEvent();
}

void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
{
   program.ChartEvent(id,lparam,dparam,sparam);
}

Теперь можно сосредоточиться на создании графического интерфейса для нашего MQL-приложения. Чтобы лучше ориентироваться в коде проекта, будем выносить некоторые методы в отдельные файлы. Создайте в папке проекта файл GUI.mqh и подключите его к файлу Program.mqh. А файл Program.mqh нужно подключить к файлу GUI.mqh, чтобы между ними установилась связь. Тоже самое будем делать в дальнейшем и с другими методами, которые можно вынести в отдельные файлы по категориям. 

Графический интерфейс будет состоять из следующих элементов:

  • Главное окно (форма).
  • Статусная строка.
  • Группа вкладок для разделов авторизации и регистрации пользователей.

На вкладке авторизации (Sign in) расположим такие элементы:

  • Кнопка для получения CSRF-токена (Get CSRF-token).
  • Кнопка для получения данных авторизованного пользователя (Get user data).
  • Кнопка для авторизации пользователя в системе (Login).
  • Кнопка для выхода из системы (Logout).
  • Поле ввода для электронного адреса пользователя, указанного при регистрации (Email).
  • Поле ввода для пароля указанного при регистрации (Password).
  • Кнопка для сохранения учётных данных пользователя в локальную базу данных для автоматического входа в систему после перезагрузки эксперта (Save credentials).
  • Кнопка для удаления учётных данных из локальной базы данных (Delete credentials).

На вкладке регистрации (Sign up) расположим следующие элементы:

  • Поле ввода для имени пользователя (Name).
  • Поле ввода для электронного адреса пользователя (Email).
  • Поле ввода для пароля пользователя (Password).
  • Кнопка для регистрации пользователя (Register new user).

Далее, определяем экземпляры элементов графического интерфейса и метод CreateGUI() для его создания:

class CProgram : public CWndCreate {
private:
   CWindow           m_window1;
   CStatusBar        m_status_bar;
   
   CTabs             m_tabs1;
   CButton           m_csrf_cookie_btn;
   CButton           m_user_btn;
   CButton           m_login_btn;
   CButton           m_logout_btn;
   CTextEdit         m_email_edit;
   CTextEdit         m_password_edit;
   CButton           m_save_credentials_btn;
   CButton           m_delete_credentials_btn;
   
   CTextEdit         m_signup_name_edit;
   CTextEdit         m_signup_email_edit;
   CTextEdit         m_signup_password_edit;
   CButton           m_register_btn;

...

public:
   bool              CreateGUI(void);

...
};

Реализацию метода CreateGUI() Вы найдёте в файле GUI.mqh. После создания графического интерфейса нужно обязательно вызвать метод CWndEvents::CompletedGUI(), который выполнив всю необходимую работу генерирует пользовательское событие ON_END_CREATE_GUI. Мы ещё вернёмся к этому моменту, когда будем рассматривать обработку событий приложения. 

bool CProgram::CreateGUI(void) {
   int win_index=0;
   if(!CWndCreate::CreateWindow(m_window1,"Authorization",1,1,250,220,true,false,true,false))
      return(false);

   int width[]={0,90};
   string text[]={"","Disconnected"};
   if(!CWndCreate::CreateStatusBar(m_status_bar,m_window1,1,23,22,text,width))
      return(false);
   
   int x_size     =232;
   int x_size_btn =114;
   
   string tabs_names1[] ={"Sign in","Sign up"};
   int    tabs_width1[] ={123,124};
   if(!CWndCreate::CreateTabs(m_tabs1,m_window1,win_index,2,42,250,200,tabs_names1,tabs_width1,TABS_TOP,true,true,2,24))
      return(false);
   
   int tab_index=0;
   
   if(!CWndCreate::CreateButton(m_csrf_cookie_btn,"Get CSRF-cookie",m_tabs1,win_index,m_tabs1,tab_index,7,10,x_size_btn))
      return(false);
   if(!CWndCreate::CreateButton(m_user_btn,"Get user data",m_tabs1,win_index,m_tabs1,tab_index,125,10,x_size_btn))
      return(false);
   if(!CWndCreate::CreateButton(m_login_btn,"Login",m_tabs1,win_index,m_tabs1,tab_index,7,35,x_size_btn))
      return(false);
   if(!CWndCreate::CreateButton(m_logout_btn,"Logout",m_tabs1,win_index,m_tabs1,tab_index,125,35,x_size_btn))
      return(false);
   
   if(!CWndCreate::CreateTextEdit(m_email_edit,"Email:",m_tabs1,win_index,m_tabs1,tab_index,false,7,65,x_size,170,"","Enter email"))
      return(false);
   if(!CWndCreate::CreateTextEdit(m_password_edit,"Password:",m_tabs1,win_index,m_tabs1,tab_index,false,7,90,x_size,170,"","Enter password"))
      return(false);
      
   if(!CWndCreate::CreateButton(m_save_credentials_btn,"Save credentials",m_tabs1,win_index,m_tabs1,tab_index,7,120,x_size_btn))
      return(false);
   if(!CWndCreate::CreateButton(m_delete_credentials_btn,"Delete credentials",m_tabs1,win_index,m_tabs1,tab_index,125,120,x_size_btn))
      return(false);
      
   tab_index=1;

   if(!CWndCreate::CreateTextEdit(m_signup_name_edit,"Name:",m_tabs1,win_index,m_tabs1,tab_index,false,7,10,x_size,170,"","Enter your name"))
      return(false);
   if(!CWndCreate::CreateTextEdit(m_signup_email_edit,"Email:",m_tabs1,win_index,m_tabs1,tab_index,false,7,35,x_size,170,"","Enter your email"))
      return(false);
   if(!CWndCreate::CreateTextEdit(m_signup_password_edit,"Password:",m_tabs1,win_index,m_tabs1,tab_index,false,7,60,x_size,170,"","Enter your password"))
      return(false);
   if(!CWndCreate::CreateButton(m_register_btn,"Register new user",m_tabs1,win_index,m_tabs1,tab_index,7,90,x_size))
      return(false);

   CWndEvents::CompletedGUI();
   return(true);
}

Вызов метода создания графического интерфейса теперь можно поместить в метод инициализации приложения:

bool CProgram::OnInitEvent(void) {
   if(!CProgram::CreateGUI()) {
      ::Print(__FUNCTION__," > Failed to create a GUI!");
      return(false);
   }
   return true;
}

Если сейчас скомпилировать и загрузить приложение на график, то Вы увидите вот такой результат:

Рис. 1 – Графический интерфейс авторизации пользователя.

Рис. 1 – Графический интерфейс авторизации пользователя.

Это всё, что нужно для создания графического интерфейса. А далее разберём дополнительные библиотеки и методы, которые понадобятся для достижения поставленных целей.


3. Работа с JSON-объектами

Для обмена данными с сервером очень часто используется формат JSON (JavaScript Object Notation). Это удобно, когда данные нужно представить в структурированном виде. По умолчанию MQL5 не поддерживает такой формат, но в базе кода есть портированная библиотека JSON Serialization and Deserialization, которая позволяет работать с таким форматом данных. Скачайте себе эту библиотеку, если у Вас её ещё нет и поместите в каталог \MetaTrader 5\MQL5\Include. Далее кратко рассмотрим, как можно использовать эту библиотеку. 

Подключаем к проекту файл JAson.mqh с классом CJAVal и объявляем экземпляр класса CJAVal.

#include <JAson.mqh>
CJAVal json;

Добавление пар ключ/значение осуществляется вот таким образом:

json["email"]    ="test@mail.com";
json["password"] ="123123123";

Если нужно получить значение из объекта CJAVal в виде строки, то делается это с помощью метода ToStr():

string value = json["email"].ToStr();

Для получения других типов данных используются соответствующие методы: ToInt(), ToDbl(), ToBool().

Чтобы конвертировать CJAVal-объект в строку (сериализовать), используйте метод Serialize().

string str_json = "";
json.Serialize(str_json);
Print(str_json);

// Результат:
// {"email":"test@mail.com","password":"123123123"}

Если Вы приняли ответ от сервера и данные пришли в формате JSON, то сначала нужно конвертировать массив типа char в строку, так как MQL-функция WebRequest() получает ответ именно в таком виде. А затем сериализованную строку нужно десериализовать в CJAVal-объект.

Допустим пришли вот такие данные:

{"email":"test@mail.com","password":"123123123"}

Значит сначала char-массив нужно конвертировать в строку, а затем полученную строку конвертировать в CJAVal-объект (десериализовать) можно вот так:

CJAVal json;

int response_code = WebRequest(method, url, request_headers, timeout, request_data, response_body, response_headers);

string str_json = CharArrayToString(response_body);
json.Deserialize(str_json);

Если ключу в качестве значения нужно присвоить массив, то сделать это можно с помощью метода Add(). Например, при добавлении double-значения можно ещё указать количество десятичных знаков:

json["a"].Add(0.5, 2);
json["a"].Add(0.9, 2);

string str_json = "";
json.Serialize(str_json);
Print(str_json);

// Результат:
// {"a":[0.50,0.90]}

Часто может понадобиться отправить или получить массив JSON-объектов. В примере ниже показано, как (1) создать массив JSON-объектов, (2) получать значения отдельных элементов массивов и (3) сериализовать весь объект или отдельные его части перед отправкой на сервер.

data[0]["a"].Add(0.5, 2);
data[0]["a"].Add(0.9, 2);
   
data[1]["b"].Add(0.1, 2);
data[1]["b"].Add(0.3, 2);
   
Print(data[0]["a"][0].ToStr());
Print(data[1]["b"][0].ToStr());

// 0.50
// 0.10
   
string str_json = "";
data[0].Serialize(str_json);
Print(str_json);

// {"a":[0.50,0.90]}
   
str_json = "";
data.Serialize(str_json);
Print(str_json);

// [{"a":[0.50,0.90]},{"b":[0.10,0.30]}]

Этих примеров вполне достаточно, чтобы Вы смогли полноценно использовать этот инструмент в большинстве случаев. 


4. Работа с регулярными выражениями

Регулярные выражения позволяют находить в тексте последовательности символов по заданным условиям. Задав шаблон поиска, получаем в итоге массив всех совпадений из исходного текста. Шаблон поиска может быть очень сложным (множество условий), но при этом умещаться в одну строку. Язык регулярных выражений предоставляет столько возможностей, что для их описания посвящают целые книги. Здесь мы лишь кратко рассмотрим некоторые из них. 

И в этот раз в базе кода можно найти библиотеку RegularExpressions для работы с регулярными выражениями средствами MQL5. Скачайте эту библиотеку и поместите в каталог торгового терминала: \MetaTrader 5\MQL5\Include

Кроме этого, в интернете можно найти несколько онлайн-инструментов, которые существенно облегчают работу с регулярными выражениями. Один из самых популярных это Regex101: https://regex101.com. Этот проект начинался, как простой хобби-проект, но сейчас это один из самых крупных сервисов для тестирования регулярных выражений в мире. 

Рис. 2 – Сервис для тестирования регулярных выражений Regex101.

Рис. 2 – Сервис для тестирования регулярных выражений Regex101.

Даже, если у Вас возникнут сложности с составлением регулярного выражения, можно обратиться за помощью к сообществу (Live Help) в чате Libera Chat (канал #regex). Если вопрос поставлен чётко, то Вы очень быстро получите готовое решение.

Рис. 3 – Канал #regex в чате Libera Chat.

Рис. 3 – Канал #regex в чате Libera Chat.

Давайте теперь рассмотрим несколько практических примеров. Допустим, у нас есть текст с таким содержанием, как показано в листинге ниже. Нужно найти все комментарии.

<!doctype html>
<html>
<!--
Comment 1
-->
<head>
        <!-- Comment 2 -->
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=Edge">
        <!-- Comment 3 -->
</head>
<body>
</body>
</html>

Прежде чем начинать писать код с регулярным выражением на MQL5 (или на любом другом языке) просто вставьте этот текст на странице для тестов в Regex101 и поэкспериментируйте там. Процесс пойдёт намного быстрее. 

Вот что в итоге получилось:

Рис. 4 – Результат работы регулярного выражения в Regex101.

Рис. 4 – Результат работы регулярного выражения в Regex101.

Ещё один пример. Нужно найти позицию курсора в тексте из статусной строки редактора кода:

Ln 1/1, Col 25/53

Для решения этой задачи подойдёт вот такое регулярное выражение:

Ln 1\/1, Col (?<col>\d+)\/\d+

Далее эти примеры реализуем на MQL5. Подключите к проекту библиотеку RegularExpressions. Лучше создать для быстрых тестов отдельный скрипт. 

#include <RegularExpressions\Regex.mqh>

Пример 1:

Добавьте в скрипт код из следующего листинга. В качестве исходного текста используется тот же вариант, который рассматривали выше в тестах на сайте Regex101. То есть повторим и здесь поиск всех комментариев в HTML-коде. Используется такое же регулярное выражение. Краткое построчное пояснение для тех, кто сталкивается с этим впервые. 

  • Создаём CRegex-объект, передав в конструктор шаблон регулярного выражения.
  • Получаем в CMatchCollection-объект все совпадения из исходного текста.
  • Из полученного результата в CMatchCollection-объекте запрашиваем перечислитель (CMatchEnumerator). 
  • Затем в цикле пока перечислитель возвращает истину (1) получаем каждое совпадение, (2) выводим результат в журнал и (3) удаляем это совпадение из исходника.
  • В конце программы нужно удалить все объекты, освобождая тем самым занятую ими память.

Каждый этап будет выводится в журнал, чтобы увидеть изменения.

string eol = "\n";
string html = 
"<!doctype html>" + eol +
"<html>" + eol +
"<!--" + eol +
"Comment 1" + eol +
"-->" + eol +
"<head>" + eol +
"       <!-- Comment 2 -->" + eol +
"       <meta charset=\"UTF-8\">" + eol +
"       <meta http-equiv=\"X-UA-Compatible\" content=\"IE=Edge\">" + eol +
"       <!-- Comment 3 -->" + eol +
"</head>" + eol +
"<body>" + eol +
"</body>" + eol +
"</html>";
string source = html;

Print("[Source]");
Print(source, eol);

Print("[Matches]");
CRegex *rgx = new CRegex("\\s*<!--[\\s\\S]*?-->");
CMatchCollection *matches = rgx.Matches(source);
CMatchEnumerator *en = matches.GetEnumerator();
while(en.MoveNext()) {
   CMatch *match=en.Current();
   Print(match.Value());
   StringReplace(source, match.Value(), "");
}
delete en;
delete rgx;
delete matches;

Print("\n[Result]");
Print(source);

В журнале Вы увидите вот такой результат. Все найденные совпадения были удалены из исходника.

...

[Result]
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
</head>
<body>
</body>
</html>

Пример 2:

В этой задаче нужно просто найти номер текущей позиции текстового курсора и вывести его в журнал. В регулярном выражении, которое здесь представлено, для поиска совпадения используется именованная группа. По названию этой группы можно затем запросить найденное значение. Для этого из созданного CRegex-объекта пробуем запустить поиск и если результат есть, обращаемся к определённой в шаблоне именованной группе (col), извлекая значение.

string source = "Ln 1/1, Col 23/56";
Print("[Source]");
Print(source);

CRegex *rgx = new CRegex("Ln 1/1, Col (?<col>\\d+)/\\d+");
CMatch *match = rgx.Match(source);
if(match.Success()) {
   Print("[Match]");
   Print(match.Groups()["col"].Value());
}
delete rgx;
delete match;

В журнал будет выведен такой результат:

[Source]
Ln 1/1, Col 23/56
[Match]
23

В завершение этого раздела добавим в класс CProgram нашего проекта метод для получения результатов поиска посредством регулярного выражения в массив типа ArrayString. Метод принимает три аргумента: (1) регулярное выражение, (2) текст, в котором нужно найти совпадения, (3) массив типа ArrayString, в который будут получены все найденные совпадения. 

#include <Arrays\ArrayString.mqh>

...

void CProgram::RegExpMatches(const string regexp, const string text, CArrayString &matches) {
   CRegex *rgx = new CRegex(regexp);
   CMatchCollection *mc = rgx.Matches(text);
   CMatchEnumerator *en = mc.GetEnumerator();
   while(en.MoveNext()) {
      CMatch *match = en.Current();
      string str = match.Value();
      StringTrimLeft(str);
      StringTrimRight(str);
      matches.Add(str);
   }
   delete en;
   delete mc;
   delete rgx;
}

Кроме этого, к архиву библиотеки RegularExpressions в базе кода приложено большое количество примеров для самостоятельного изучения. 

Итак. У нас есть все необходимые инструменты для дальнейшей работы над MQL-приложением. На текущий момент они уже должны быть подключены к проекту.


5. Локальная база данных SQLite

В этом разделе рассмотрим методы для работы с базой данных в разрабатываемом MQL-приложении. Создадим таблицу для хранения в ней учётных данных пользователя. Это может показаться немного избыточным, так как это будет пока единственная таблица в нашей базе данных. Но, как краткий пример для статьи вполне подойдёт, если Вы ещё не начали разбираться с этим. Данные также можно хранить в глобальных переменных терминала или в файлах. База данных всё же более приемлемый вариант с точки зрения возможностей и простоты использования. 

MQL5 предлагает нам «из коробки» использовать базу данных на движке SQLite. В документации, как всегда, Вы найдёте подробные описания и базовые примеры. В этой статье будут представлены более лаконичные и компактные реализации методов для работы с базой данных в рамках текущего проекта.

Сначала рассмотрим общие методы, которые могут использовать для всех таблиц и начнём с метода создания базы данных. Для этого будет использоваться метод CProgram::OpenDB(). Этот метод принимает один аргумент – имя базы данных. Если такой базы данных ещё нет, то она создаётся с помощью функции DatabaseOpen() и на это указывает флаг DATABASE_OPEN_CREATE. Если же уже есть, то открывается с указанными для открытия свойствами: (1) режим на чтение/запись (DATABASE_OPEN_READWRITE), (2) расположение в общей папке терминала (DATABASE_OPEN_COMMON). Метод CProgram::OpenDB() возвращает хендл, который нужно использовать для дальнейшей работы с базой данных, либо отрицательное значение, если попытка открытия/создания завершилась ошибкой. 

int CProgram::OpenDB(const string file_name) {
   ResetLastError();
   int db = DatabaseOpen(file_name, 
      DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON
   );
   if(db == INVALID_HANDLE) {
      Print("DB: ", file_name, " open failed with code ", GetLastError());
      return -1;
   }
   return db;
}

К категории общих методов также отнесём метод для удаления таблицы - CProgram::DeleteTable(). Принимает аргументы с хендлом базы данных и именем таблицы. Удаление таблицы осуществляется с помощью функции DatabaseExecute(), которая также принимает хендл и соответствующий SQL-запрос. 

bool CProgram::DeleteTable(int db, string table_name) {
   ResetLastError();
   if(!DatabaseExecute(db, "DROP TABLE IF EXISTS "+table_name)) {
      Print("Failed to drop the "+table_name+" table with code ", GetLastError());
      return(false);
   }
   return(true);
}

Ещё один общий метод, который может пригодиться для подсчёта строк в результате запросов к таблицам базы данных - CProgram::NumberOfLinesRecieved(). Принимает хендл базы данных и SQL-запрос. Сначала получаем хендл запроса с помощью функции DatabasePrepare(), передав в неё те же аргументы. В случае неудачи, закрываем соединение с базой данных и возвращаем отрицательное значение. Если же хендл запроса получен, подсчитываем в цикле количество полученных позиций в результате SQL-запроса. В конце хендл запроса удаляется и отдаётся результат. 

long CProgram::NumberOfLinesRecieved(const int db, const string sql) {
   ResetLastError();
   int request = DatabasePrepare(db, sql);
   if(request == INVALID_HANDLE) {
      Print("Request failed with code ", GetLastError());
      DatabaseClose(db);
      return -1;
   }
   long counter = 0;
   while(DatabaseRead(request)) {
      counter++;
   }
   DatabaseFinalize(request);
   return counter;
}

Далее рассмотрим методы, которые будут использоваться для работы с таблицей CREDENTIALS. В этой таблице будут храниться учётные данные пользователя. Всего получилось 7 методов:

  • CreateTableCredentials – создаёт в базе данных таблицу CREDENTIALS.
  • InsertCredentials – добавляет строки в таблицу с учётными данными пользователя.
  • UpdateCredentials – обновляет значения учётных данных.
  • UpdatingCredentialsIfChanged – обновляет учётные данные, если они изменились.
  • GetCredentials – получение учётных данных в CJAVal-объект.
  • SaveCredentials – сохраняет учётные данные в базу данных.
  • DeleteCredentials – удаляет учётные данные в базе данных.

Все эти методы вынесем в отдельный файл Credentials.mqh и подключим его к файлу приложения Program.mqh. Теперь разберём подробнее, как это всё работает. 

Нам понадобится структура (Credentials) для учётных данных пользователя. В ней должно быть три поля: (1) идентификатор строки (id), (2) название параметра (param) и (3) значение параметра (value).

struct Credentials {
   int      id;
   string   param;
   string   value;
};

Метод CreateTableCredentials(). Сначала с помощью функции DatabaseTableExists() проверяем, есть ли в базе данных таблица CREDENTIALS и если её ещё нет, то функция DatabaseExecute() выполняет SQL-запрос для создания таблицы. 

class CProgram : public CWndCreate {
 private:
   string            m_db_name;
   string            m_credentials_table;
...
};

CProgram::CProgram(void) : m_db_name("mt5.sqlite"),
                           m_credentials_table("CREDENTIALS")
{
}

bool CProgram::CreateTableCredentials(const int db) {
   ResetLastError();
   if(!DatabaseTableExists(db, m_credentials_table)) {
      if(!DatabaseExecute(db, "CREATE TABLE "+m_credentials_table+"("
                          "ID INT PRIMARY KEY NOT NULL,"
                          "PARAM TEXT NOT NULL,"
                          "VALUE TEXT);"))
      {
         Print("Create table failed with code ", GetLastError());
         DatabaseClose(db);
         return false;
      }
   }
   return true;
}

Успешность создания таблицы можно проверить в навигаторе редактора кода MetaEditor на вкладке Database.

Рис. 5 – Просмотр таблиц базы данных в редакторе кода MetaEditor.

Рис. 5 – Просмотр таблиц базы данных в редакторе кода MetaEditor.

Метод CProgram::InsertCredentials(). Здесь сначала спрашиваем у пользователя нужно ли сохранить данные, которые он указал для авторизации. Если пользователь отвечает утвердительно, то функция DatabaseExecute() выполняет SQL-запрос для добавления в таблицу CREDENTIALS двух строк с электронным почтовым адресом и паролем пользователя. 

bool CProgram::InsertCredentials(const int db) {
   int res = MessageBox(
      "Save credentials for the next login?", 
      "Saving credentials", MB_YESNO
   );
   if(res == IDYES) {
      ResetLastError();
      if(!DatabaseExecute(db, 
         "INSERT INTO "+m_credentials_table+" (ID,PARAM,VALUE) VALUES (1,'email','"+m_email_edit.GetValue()+"'); "
         "INSERT INTO "+m_credentials_table+" (ID,PARAM,VALUE) VALUES (2,'password','"+m_password_edit.GetValue()+"');"
      )) {
         Print("Insert failed with code ", GetLastError());
         DatabaseClose(db);
         return false;
      }
   }
   return true;
}

Метод CProgram::UpdateCredentials() отличается от предыдущего только SQL-запросом:

...

         "UPDATE "+m_credentials_table+" SET value = '"+m_email_edit.GetValue()+"' WHERE param = 'email';"
         "UPDATE "+m_credentials_table+" SET value = '"+m_password_edit.GetValue()+"' WHERE param = 'password';"

...

Метод CProgram::GetCredentials(). В начале создаём CJAVal-объект и добавляем в него ключи учётных данных. Затем открываем для работы базу данных. Если таблица CREDENTIALS уже создана, то определяем SQL-запрос на поиск строк в таблице со значениями email и password в столбце PARAM. Если ошибок нет, то в цикле на каждой итерации получаем в структуру Credentials все значения строки результата SQL-запроса. Для этого используется функция DatabaseReadBind(). И затем сохраняем значения учётных данных в CJAVal-объект в соответствии с их названиями ключей. После этого хендл запроса удаляется, база данных закрывается и возвращается CJAVal-объект с учётными данными пользователя.

CJAVal CProgram::GetCredentials(void) {
   CJAVal data;
   data["email"]    ="";
   data["password"] ="";
   
   int db = -1;
   if((db = CProgram::OpenDB(m_db_name)) < 0) {
      return false;
   }
   
   ResetLastError();
   if(DatabaseTableExists(db, m_credentials_table)) {
      int request = DatabasePrepare(db, 
         "SELECT * FROM "+m_credentials_table+" WHERE PARAM = 'email' OR PARAM = 'password'"
      );
      if(request == INVALID_HANDLE) {
         Print("Request failed with code ", GetLastError());
         DatabaseClose(db);
         return false;
      }
      Credentials credentials;
      for(int i=0; DatabaseReadBind(request, credentials); i++) {
         if(credentials.param == "email") {
            data["email"] = credentials.value;
         }
         else if(credentials.param == "password") {
            data["password"] = credentials.value;
         }
      }
      DatabaseFinalize(request);
   }
   DatabaseClose(db);
   return data;
}

Метод CProgram::UpdatingCredentialsIfChanged(). Обновление учётных данных в этом методе осуществляется только в том случае, если вводимые пользователем данные отличаются от тех, которые сейчас в таблице CREDENTIALS

bool CProgram::UpdatingCredentialsIfChanged(const int db) {
   int request = DatabasePrepare(db, 
      "SELECT * FROM "+m_credentials_table+" WHERE PARAM = 'email' OR PARAM = 'password'"
   );
   if(request == INVALID_HANDLE) {
      Print("Request failed with code ", GetLastError());
      DatabaseClose(db);
      return false;
   }
   bool is_changed = false;
   Credentials cr[];
   Credentials credentials;
   for(int i=0; DatabaseReadBind(request, credentials); i++) {
      int size = ArraySize(cr);
      ArrayResize(cr, size+1);
      cr[size] = credentials;
      if((credentials.param == "email" && credentials.value != m_email_edit.GetValue()) ||
         (credentials.param == "password" && credentials.value != m_password_edit.GetValue())) {
         is_changed = true;
      }
   }
   DatabaseFinalize(request);
   
   if(is_changed) {
      CProgram::UpdateCredentials(db);
   }
   return true;
}

Метод CProgram::SaveCredentials(). Здесь используются некоторые методы, которые рассматривались выше. Сначала открывается база данных. Затем создаётся таблица CREDENTIALS, если её ещё нет. Далее проверяется, есть ли в таблице поля email и password в столбце PARAM. Если их нет, то будут добавлены. Если уже есть, то будут обновлены.

void CProgram::SaveCredentials(void) {
   int db = -1;
   if((db = CProgram::OpenDB(m_db_name)) < 0) {
      return;
   }
   
   if(!CProgram::CreateTableCredentials(db)) {
      return;
   }
   
   long line_total = 0;
   if((line_total = CProgram::NumberOfLinesRecieved(db, 
         "SELECT * FROM "+m_credentials_table+" WHERE PARAM = 'email' OR PARAM = 'password'")) < 0) {
      return;
   }
   if(!line_total) {
      if(!CProgram::InsertCredentials(db)) {
         return;
      }
   } else {
      if(!CProgram::UpdatingCredentialsIfChanged(db)) {
         return;
      }
   }
   DatabaseClose(db);
}

Метод CProgram::DeleteCredentials(). Перед тем, как удалить учётные данные из таблицы CREDENTIALS, открывается диалоговое окно с вопросом, действительно ли пользователь хочет удалить сохранённые ранее учётные данные. Если пользователь отвечает утвердительно, то для значений учётных данных устанавливаются пустые строки, и в базе данных, и в графическом интерфейсе. 

bool CProgram::DeleteCredentials(void) {
   int db = -1;
   if((db = CProgram::OpenDB(m_db_name)) < 0) {
      return false;
   }
   int res = MessageBox(
      "Do you really want to delete your credentials?\nAfter you delete them, you will have to enter them yourself.", 
      "Deleting credentials", MB_YESNO
   );
   if(res == IDYES) {
      ResetLastError();
      if(!DatabaseExecute(db, 
         "UPDATE "+m_credentials_table+" SET value = '' WHERE param = 'email';"
         "UPDATE "+m_credentials_table+" SET value = '' WHERE param = 'password';"
      )) {
         Print("Delete failed with code ", GetLastError());
         DatabaseClose(db);
         return false;
      }
      m_email_edit.SetValue("");
      m_password_edit.SetValue("");
      CWndEvents::Update(true);
   }
   DatabaseClose(db);
   return true;
}

Мы рассмотрели все методы для работы с базой данных в этом проекте. Далее рассмотрим ещё одну группу методов, которая относится к авторизации пользователя и HTTP-запросам к серверной части разрабатываемого приложения. 


6. HTTP-запросы к серверной части приложения

В статье Веб-проекты (Часть II): Система авторизации Laravel/Nuxt были показаны инструменты для тестирования маршрутов относящихся к авторизации пользователя. Для отслеживания сетевого трафика, который приходит/уходит в/из MetaTrader 5 нам понадобится дополнительный инструмент.

6.1. Анализатор сетевых протоколов Wireshark

Существует множество различных анализаторов сетевых протоколов, но самым продвинутым на текущий момент является Wireshark. Установите это бесплатное программное обеспечение, если у Вас его ещё нет. Запустив Wireshark Вы увидите список сетевых интерфейсов, как показано на скриншоте ниже. Выберите пункт с Adapter for loopback traffic capture двойным щелчком или через контекстное меню правой кнопкой мыши (Start capture). 

Рис. 6 – Анализатор сетевых протоколов - Wireshark.

Рис. 6 – Анализатор сетевых протоколов - Wireshark.

Далее Вы увидите весь список захваченных пакетов, который постоянно обновляется:

Рис. 7 – Список захваченных пакетов в Wireshark.

Рис. 7 – Список захваченных пакетов в Wireshark.

Сейчас нас интересуют только HTTP-запросы, поэтому этот список лучше пропустить через фильтр. Для этого введите в поле ввода сверху «http» и нажмите на кнопку Apply display filter справа. Если список пустой, то это нормально. Значит HTTP-запросов пока ещё не было от момента начала текущей сессии. Чуть позже вернёмся к этому, а пока рассмотрим MQL-методы, которые нам понадобятся для работы.

6.2. Методы для HTTP-запросов

Группу методов относящихся к HTTP-запросам для удобства вынесем в отдельный файл - Requests.mqh. В начале файла определим константы для точек доступа API

#define BASE_URL     "http://localhost:8000"
#define BASE_API_URL "http://localhost:8000/api"

Также нам понадобится вспомогательный метод CProgram::PrintResponse() для вывода в журнал данных, которые отдаются сервером. Может быть полезным изучить заголовки ответа и посмотреть объём данных. Так как мы используем для серверной части фреймворк Laravel, то в случае возникновения ошибки могут приходить очень большие листинги отладочной информации. Здесь мы ограничиваем вывод указанным количеством строк (10 по умолчанию) для таких случаев. 

void CProgram::PrintResponse(string response_headers, 
                              char &response_body[], 
                              int response_code, 
                              int line_total = 10) {
   Print("\n[response_headers]");
   Print(response_headers);

   Print("[response_body]");
   string body = CharArrayToString(response_body);
   string sbody[];
   string sep="\n";
   ushort u_sep;
   u_sep=StringGetCharacter(sep, 0);
   StringSplit(body, u_sep, sbody);
   
   Print("string length: ", StringLen(body));
   Print("array size: ", ArraySize(sbody));
   
   if(response_code >= 200 && response_code < 300) {
      Print(body);
      return;
   }
   if(response_code >= 300 && response_code < 600) {
      for(int i=0; i<ArraySize(sbody); i++) {
         Print(sbody[i]);
         if(i > line_total) {
            Print("...");
            break;
         }
      }
   }
}

В качестве общего метода для HTTP-запросов используется метод CProgram::Request(). Он выполняет вызов функции WebRequest() и просто предназначен для удобства и сокращения кода. Используется вторая версия вызова WebRequest(), когда заголовки запроса можно сформировать перед отправкой. 

int CProgram::Request(string method, string url, 
                        string request_headers, int timeout, char &request_data[],
                        char &response_body[], string &response_headers) 
{
   ResetLastError();
   int response_code = 
      WebRequest(method, url, 
         request_headers, timeout, request_data, 
         response_body, response_headers);
   
   int error =GetLastError();
   Print("\nresponse_code: ", response_code, ", error: ", error);

   if(response_code < 0 || error > 0) {
      CProgram::PrintResponse(response_headers, response_body, response_code);
      return -1;
   }
   
   return response_code;
}

Основные же действия будут происходить в следующих методах:

  • RequestCsrfCookie() – запрос для получения XSRF-токена.
  • RequestLogin() – запрос для авторизации пользователя.
  • RequestUserData() – запрос на получение данных авторизованного пользователя.
  • RequestLogout() – запрос для выхода из системы.
  • RequestRegister() – запрос для регистрации пользователя.
  • GetHeaders() – получение сформированных заголовков для запроса.
  • SaveXsrfData() – сохранение токена доступа для использования в последующих запросах.

Это минимальный набор для создания простой авторизации. Далее разберём подробнее, как это всё работает. Кроме описания устройства методов будем также рассматривать результаты запросов и ответов.

class CProgram : public CWndCreate {
 private:
   string            m_xsrf_token;
   int               m_last_response_code;

...

private:
   bool              RequestCsrfCookie();
   bool              RequestLogin();
   bool              RequestUserData();
   bool              RequestLogout();
   bool              RequestRegister();

   string            GetHeaders(void);
   void              SaveXsrfData(string response_headers);
};

CProgram::CProgram(void) : m_xsrf_token(""),
                           m_last_response_code(-1)
{
}

Графический интерфейс уже построен, а обработчики событий привязаны к кнопкам для вызова перечисленных выше методов (см. листинг кода ниже). Кнопка GET CSRF-cookie для получения XSRF-токена добавлена в графический интерфейс для демонстрации и тестов, так как само по себе получение токена не имеет никакого смысла и должно осуществляться в методах, где это необходимо, чтобы запрос прошёл успешно.

void CProgram::OnEvent(const int id,const long &lparam,
                        const double &dparam,const string &sparam) {

...

//--- Button press events
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) {
      //--- Sign in tab
      if(lparam==m_csrf_cookie_btn.Id()) {
         CProgram::RequestCsrfCookie();
         return;
      }
      if(lparam==m_user_btn.Id()) {
         CProgram::RequestUserData();
         return;
      }
      if(lparam==m_login_btn.Id()) {
         CProgram::RequestLogin();
         return;
      }
      if(lparam==m_logout_btn.Id()) {
         CProgram::RequestLogout();
         return;
      }
      if(lparam==m_save_credentials_btn.Id()) {
         CProgram::SaveCredentials();
         return;
      }
      if(lparam==m_delete_credentials_btn.Id()) {
         CProgram::DeleteCredentials();
         return;
      }
      //--- Sign up tab
      if(lparam==m_register_btn.Id()) {
         CProgram::RequestRegister();
         return;
      }
      return;
   }
}

Начнём с метода CProgram::RequestCsrfCookie() для получения XSRF-токена. Для того, чтобы авторизация пользователя прошла успешно, кроме электронного адреса и пароля нужно ещё иметь в наличии токен доступа. Его можно получить по маршруту /sanctum/csrf-cookie. Сервер отдаёт эти данные в заголовках ответа, которые в случае успешного выполнения запроса передаются для сохранения в метод CProgram::SaveXsrfData() и затем на вывод в журнал. 

bool CProgram::RequestCsrfCookie(void) {
   string url = BASE_API_URL + "/sanctum/csrf-cookie";
   char   request_data[];
   char   response_body[];
   string response_headers ="";

   if((m_last_response_code = this.Request("GET", url, 
         "", NULL, request_data, 
         response_body, response_headers)) < 0) {
      return false;
   }
   
   CProgram::SaveXsrfData(response_headers);
   CProgram::PrintResponse(response_headers, response_body, m_last_response_code);
   return true;
}

Если сейчас попробовать сделать запрос к маршруту /sanctum/csrf-cookie, нажав кнопку Get CSRF-cookie, то в журнал MetaTrader 5 (вкладка Experts) будет выведена следующая информация (см. листинг ниже). Данные, которые нам нужны находятся в заголовках Set-Cookie (XSRF-TOKEN) и теперь его нужно оттуда извлечь на сохранение.

response_code: 204, error: 0

[response_headers]
Host: localhost
Date: Sat, 05 Feb 2022 11:30:39 GMT
Connection: close
X-Powered-By: PHP/7.4.6
Cache-Control: no-cache, private
Date: Sat, 05 Feb 2022 11:30:39 GMT
Vary: Origin
Set-Cookie: XSRF-TOKEN=eyJpdiI6IjdHUGRTc3...3D; expires=Sat, 05-Feb-2022 13:30:39 GMT; Max-Age=7200; path=/; samesite=lax
Set-Cookie: laravel_session=eyJpdiI6InlRT...3D; expires=Sat, 05-Feb-2022 13:30:39 GMT; Max-Age=7200; path=/; httponly; samesite=lax

[response_body]
string length: 0
array size: 0

В анализаторе Wireshark изучать данные запросов и ответов удобнее, так как вся информация структурирована и сгруппирована. 

Рис. 8 – Данные ответа в анализаторе Wireshark.

Рис. 8 – Данные ответа в анализаторе Wireshark.

Ещё один вариант представления данных для изучения заголовков. Кликните правой кнопкой на пакете, вызвав контекстное меню, где можно выбрать пункт Follow -> HTTP Stream. Это действие откроет окно, в котором наглядно представлен весь обмен между двумя узлами.

Рис. 9 – Наглядное представление данных между двумя узлами.

Рис. 9 – Наглядное представление данных между двумя узлами.

Извлечение данных из заголовков Set-Cookie осуществляется в методе CProgram::SaveXsrfData() следующим образом. Метод принимает аргумент с заголовками ответа сервера, в которых нужно найти XSRF-токен. Он передаётся в заголовке Set-Cookie и самый простой способ его извлечь будет использовать регулярные выражения. Создаём объект регулярного выражения с шаблоном из трёх групп: (1) начало, с которого начинается строка (заголовок), (2) именованная группа token, где и будет содержаться токен, (3) окончание строки (заполнитель до нужного количества). Затем выполняется поиск в переданных в метод заголовках и, если совпадение найдено, то сохраняем полученный токен для последующего использования. 

void CProgram::SaveXsrfData(string response_headers) {
   CRegex *rgx = new CRegex("(Set-Cookie:\\s*XSRF-TOKEN=)(?<token>.+?)(%3D;)");
   CMatch *m=rgx.Match(response_headers);
   if(m.Success()) {
      m_xsrf_token = m.Groups()["token"].Value();
   }
   delete m;
   delete rgx;
}

При последующих запросах полученный XSRF-токен нужно добавлять в заголовки запроса и для этого тоже есть вспомогательный метод CProgram::GetHeaders(). Кроме заголовка X-XSRF-TOKEN нужно также добавить заголовки Accept и Referer

  • Accept – указывает, какие типы контента может понять клиент.
  • Referer – адрес исходной страницы, с которой был осуществлён запрос.

string CProgram::GetHeaders(void) {
   string eol = "\r\n";
   string headers = "";
   headers += (m_xsrf_token != ""? "X-XSRF-TOKEN: " + m_xsrf_token + eol : "");
   headers += "Accept: application/json" + eol;
   headers += "Referer: localhost:3000";
   return headers;
}

Здесь следует отметить, что на момент написания этой статьи заголовок Accept не передаётся терминалом MetaTrader 5, как ожидается. Это можно отследить в анализаторе Wireshark. То есть значение Accept всегда будет передаваться, как */*, игнорируя значение, которое указывается при формировании этого заголовка в MQL. Из-за этого сервер отдаёт в качестве ответа данные, которые относятся к служебной или отладочной информации, а не в формате JSON, как запрашивается. Это относится только к тем случаям, где вариантов ответа от сервера может быть несколько. Приоритет отдаётся в данном случае к типу text/html (см. заголовок ответа Content-Type). В тех же случаях, когда ответ от сервера отдаётся только в формате JSON (application/json), то тогда тип данных приходит такой, как запрашивается. 

Рис. 10 – Заголовок Accept с указанием на приём значения любого MIME-типа.

Рис. 10 – Заголовок Accept с указанием на приём значения любого MIME-типа.

Теперь рассмотрим метод CProgram::RequestLogin() для авторизации пользователя. Здесь в самом начале, если какое-то из полей формы будет не заполнено, выйдет подсказка об этом. Затем делается предварительный запрос на получение XSRF-токена, без которого не получится осуществить вход в систему (код ответа 419 - CSRF token mismatch). Но здесь также есть несоответствие с тем, что ожидается получить, как это получается в браузере либо программе Postman. Результат этого запроса в терминале MetaTrader 5 будет показывать ошибку 500

После получения XSRF-токена формируются заголовки и учётные данные для авторизации пользователя. Учётные данные можно передать, как сериализованный JSON-объект, но в этом случае показан пример, когда они передаются в виде параметров запроса. Обязательно нужно конвертировать строку с параметрами в char-массив. После этого осуществляется POST-запрос на авторизацию и, если всё прошло успешно, то (1) сохраняем полученный новый XSRF-токен для следующего запроса, (2) выводим информацию в журнал, (3) в статусной строке показываем текст «Connected» и (4) сохраняем введённые учётные данные пользователя для следующего входа. 

bool CProgram::RequestLogin(void) {
   if(m_email_edit.GetValue() == "" || m_password_edit.GetValue() == "") {
      int res = MessageBox(
         "Email or Password is empty!", 
         "Error"
      );
      return false;
   }
   
   if(!CProgram::RequestCsrfCookie()) {
      return false;
   }
   
   string url = BASE_API_URL + "/login";
   string request_headers = CProgram::GetHeaders();
   string request_params = 
      "email=" + m_email_edit.GetValue() + 
      "&password=" + m_password_edit.GetValue();
   char   request_data[];
   char   response_body[];
   string response_headers;
   
   Print("[request_headers]");
   Print(request_headers);
   
   ArrayResize(request_data, 
      StringToCharArray(request_params,request_data,0,WHOLE_ARRAY,CP_UTF8)-1
   );
   
   if((m_last_response_code = this.Request("POST", url,
         request_headers, 10000, request_data, 
         response_body, response_headers)) < 0) {
      return false;
   }
   CProgram::SaveXsrfData(response_headers);
   CProgram::PrintResponse(response_headers, response_body, m_last_response_code);
   
   m_status_bar.SetValue(1, "Connected");
   m_status_bar.GetItemPointer(1).Update(true);
   
   CProgram::SaveCredentials();
   return true;
}

Метод CProgram::RequestUserData() предназначен для получения данных пользователя. Здесь также формируются заголовки перед запросом. В них будет содержаться XSRF-токен, который был сохранён после предыдущего запроса. После выполнения GET-запроса также не забываем его сохранить. Такие же действия нужно будет выполнять во всех запросах, чтобы всегда получать актуальные данные. Если в результате успешного запроса получен код 200, конвертируем полученные данные в строку, десериализуем её в JSON-объект (CJAVal) и покажем полученные данные пользователя в диалоговом окне в качестве демонстрации.

bool CProgram::RequestUserData(void) {
   string url = BASE_API_URL + "/user";
   string request_headers = CProgram::GetHeaders();
   char   request_data[];
   char   response_body[];
   string response_headers;
   
   Print("[request_headers]");
   Print(request_headers);
   
   if((m_last_response_code = this.Request("GET", url, 
         request_headers, NULL, request_data, 
         response_body, response_headers)) < 0) {
      return false;
   }
   CProgram::SaveXsrfData(response_headers);
   CProgram::PrintResponse(response_headers, response_body, m_last_response_code);
   
   if(m_last_response_code == 200) {
      CJAVal data;
      data.Deserialize(CharArrayToString(response_body));
      string user_data = "";
      user_data += "id:\t\t" + data["id"].ToStr();
      user_data += "\nname:\t\t" + data["name"].ToStr();
      user_data += "\nemail:\t\t" + data["email"].ToStr();
      user_data += "\nemail_verified_at:\t" + 
         ((data["email_verified_at"].ToStr() == "")? 
            "-" : 
            string(StringToTime(data["email_verified_at"].ToStr())));
      user_data += "\ncreated_at:\t" + string(StringToTime(data["created_at"].ToStr()));
      user_data += "\nupdated_at:\t" + string(StringToTime(data["updated_at"].ToStr()));
      
      int res = MessageBox(
         user_data, "User data"
      );
   }
   return true;
}

Метод CProgram::RequestLogout() в начале включает в себя такие же действия, как показано в предыдущем листинге. Здесь делается GET-запрос для выхода из системы. Затем, ранее сохранённый токен обнуляется, а в статусной строке показывается текст «Disconnected». Дополнительную информацию о запросе и ответе можно изучить в журнале на вкладке Experts

bool CProgram::RequestLogout(void) {
   string url = BASE_API_URL + "/logout";
   string request_headers = CProgram::GetHeaders();
   uchar  request_data[];
   uchar  response_body[];
   string response_headers;
   
   Print("[request_headers]");
   Print(request_headers);
   
   if((m_last_response_code = this.Request("GET", url, 
         request_headers, NULL, request_data, 
         response_body, response_headers)) < 0) {
      return false;
   }
   m_xsrf_token = "";
   
   CProgram::PrintResponse(response_headers, response_body, m_last_response_code);
   
   m_status_bar.SetValue(1, "Disconnected");
   m_status_bar.GetItemPointer(1).Update(true);
   return true;
}

Метод CProgram::RequestRegister(). Регистрация пользователя не требует XSRF-токена, поэтому здесь не выполняется вызов метода CProgram::RequestCsrfCookie(). Результат ответа содержит в себе данные пользователя, которые он ввёл в форме. В остальном этот метод похож на предыдущие и не требует пояснений. 

bool CProgram::RequestRegister(void) {
   string url = BASE_API_URL + "/register";
   string request_headers = CProgram::GetHeaders();
   string request_params = 
      "name=" + m_signup_name_edit.GetValue() + 
      "&email=" + m_signup_email_edit.GetValue() + 
      "&password=" + m_signup_password_edit.GetValue();
   char   request_data[];
   char   response_body[];
   string response_headers;
   
   Print("[request_headers]");
   Print(request_headers);
   
   ArrayResize(request_data, 
      StringToCharArray(request_params,request_data,0,WHOLE_ARRAY,CP_UTF8)-1
   );
   
   if((m_last_response_code = this.Request("POST", url,
         request_headers, 10000, request_data, 
         response_body, response_headers)) < 0) {
      return false;
   }
   CProgram::SaveXsrfData(response_headers);
   CProgram::PrintResponse(response_headers, response_body, m_last_response_code);
   return true;
}

Это все методы для выполнения HTTP-запросов в этом приложении и прежде чем перейти к завершающему разделу этой статьи, установите в настройках терминала возможность отправлять HTTP-запросы:

Рис. 11 - Настройки для отправки HTTP-запросов.

Рис. 11 - Настройки для отправки HTTP-запросов.

На этой анимации показано, как происходит авторизация пользователя в системе через пользовательский GUI в терминале MetaTrader 5:

Рис. 12 - Демонстрация авторизации пользователя в терминале MetaTrader 5.

Рис. 12 - Демонстрация авторизации пользователя в терминале MetaTrader 5.

6.3. Автоматический вход в систему

В качестве дополнения можно продемонстрировать, как сделать автоматическую авторизацию пользователя при загрузке этого MQL-приложения на график. Когда графический интерфейс пользователя полностью создан, генерируется пользовательское событие ON_END_CREATE_GUI, которое можно отследить в обработчике событий. Если событие пришло сюда, то пробуем получить учётные данные пользователя из базы данных приложения. Данные возвращаются в виде CJAVal-объекта. Затем, если это не пустые строки они вставляются в поля ввода формы авторизации и делается запрос на вход пользователя в систему. 

void CProgram::OnEvent(const int id,const long &lparam,
                        const double &dparam,const string &sparam) 
{
   if(id==CHARTEVENT_CUSTOM+ON_END_CREATE_GUI) {
   
      CJAVal credentials = CProgram::GetCredentials();
      
      if(credentials["email"].ToStr() != "" && credentials["password"].ToStr() != "") {
         m_email_edit.SetValue(credentials["email"].ToStr());
         m_password_edit.SetValue(credentials["password"].ToStr());
         CWndCreate::Update(true);
      
         CProgram::RequestLogin();
      }
      return;
   }
}

На этом можно закончить эту статью. Если изучаете эту тему впервые, то рекомендуется всё тщательно протестировать самостоятельно прежде, чем использовать в своих проектах. 


7. Заключение

Полная реализация системы авторизации пользователя описана в двух статьях:

В первой главе была продемонстрирована реализация системы авторизации Laravel Sanctum, как браузерное веб-приложение. А во второй главе в эту схему интегрировался торговый терминал MetaTrader 5. В следующей статье будет представлена реализация MQL-приложения для мгновенного обмена сообщениями (WebSockets) между сервером, браузером и терминалом MetaTrader 5.


Прикрепленные файлы |
mt5-auth.zip (7.74 KB)
mt5-web-project.zip (232.78 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (10)
Renat Akhtyamov
Renat Akhtyamov | 19 мая 2022 в 20:45
+
Jonathan Pereira
Jonathan Pereira | 12 сент. 2022 в 15:58
Anatoli Kazharski
Anatoli Kazharski | 12 сент. 2022 в 16:04
Jonathan Pereira #:

вы используете другую версию для этой библиотеки?

Baixe 'RegularExpressions in MQL5 for working with regular expressions' from 'MetaQuotes' for MetaTrader 5 gratuitamente na Base de Código MQL5, 2016.05.20

Да, всё верно. 

...

И в этот раз в базе кода можно найти библиотеку RegularExpressions для работы с регулярными выражениями средствами MQL5. Скачайте эту библиотеку и поместите в каталог торгового терминала: \MetaTrader 5\MQL5\Include

...

Jonathan Pereira
Jonathan Pereira | 12 сент. 2022 в 16:21

Я скачал эту ссылку, но я получаю эту ошибку:



Я думал, что у вас была эта ошибка, и, возможно, вы исправили ее на своем компьютере.

Anatoli Kazharski
Anatoli Kazharski | 12 сент. 2022 в 16:42
Jonathan Pereira #:

Я скачал эту ссылку, но я получаю эту ошибку:

...

Я думал, что у вас была эта ошибка, и, возможно, вы исправили ее на своем компьютере.

Таких ошибок не возникало на тот момент.

Обычно, если вношу, какие-то изменения в сторонний код, то указываю об этом в статье. 

К сожалению, пока нет времени, чтобы разобраться с этим. Возможно, были какие-то изменения в файле DynamicMatrix.mqh из стандартной библиотеки, который используется в RegularExpressions:

#include <Internal\DynamicMatrix.mqh>

//--- 

В коде стандартной библиотеки я точно ничего не исправлял. А если это делаю, то только в копии, которую подключаю к проекту отдельно, так как после обновления терминала файлы стандартной библиотеки заменяются новыми версиями и все внесённые изменения будут утеряны.

Графика в библиотеке DoEasy (Часть 97): Независимая обработка перемещения объектов-форм Графика в библиотеке DoEasy (Часть 97): Независимая обработка перемещения объектов-форм
В статье рассмотрим реализацию независимого перемещения мышкой любых объектов-форм, а также дополним библиотеку сообщениями об ошибках и новыми свойствами сделок, ранее уже введёнными в терминал и MQL5.
Шаблон проектирования MVC и возможность его использования (Часть 2): Схема взаимодействия между тремя компонентами Шаблон проектирования MVC и возможность его использования (Часть 2): Схема взаимодействия между тремя компонентами
Данная статья продолжает и завершает тему, поднятую в прошлой статье — шаблон MVC в программах на MQL. В этой статье мы рассмотрим возможную схему взаимодействия между этими тремя компонентами.
Уроки по DirectX (Часть I): Рисуем первый треугольник Уроки по DirectX (Часть I): Рисуем первый треугольник
Это вводная статья по DirectX, которая описывает особенности работы с API. Помогает разобраться с порядком инициализации его компонентов. Приводит пример написания скрипта на MQL, выводящего треугольник с помощью DirectX.
Использование класса CCanvas в MQL приложениях Использование класса CCanvas в MQL приложениях
Статья об использовании класса CCanvas в MQL приложениях с подробным разбором и примерами, что даёт пользователю понимание основ работы с данным инструментом