Сервисы

Сервисом является MQL-программа с единственным обработчиком OnStart и директивой #property service.

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

В качестве примера сервиса решим небольшую прикладную проблему, которая часто возникает у разработчиков MQL-программ. Многие из них практикуют привязку своих программ к номеру счета пользователя. Речь здесь не обязательно идет о платном продукте, а может касаться распространения среди друзей и знакомых для сбора статистики или удачных настроек. При этом пользователь может помимо рабочего реального счета регистрировать демо-счета. Время существования таких счетов, как правило, ограничено, и потому обновлять ради них привязку каждую пару недель довольно неудобно. Для этого надо править исходный код, компилировать и отсылать программу заново.

Вместо этого мы можем разработать сервис, который будет регистрировать в глобальных переменных (или файлах) номера счетов, к которым произошло успешное подключение из данного терминала.

Технология привязки основывается на попарном шифровании (или, как вариант, хешировании) номеров счетов: прежнего счета логина и нового счета логина. Предыдущий счет должен быть мастер-счетом (на который "выписана" условная привязка), чтобы общая подпись пары расширяла права пользования продуктом на новый счет. В качестве ключа используется секрет, известный только внутри программ (предполагается, что все они поставляются в закрытом, откомпилированном виде). Результатом операции будет строка в формате Base64. В реализации применены функции MQL5 API, часть которых еще предстоит изучить, в частности, получение номера счета через AccountInfoInteger и шифрование функцией CryptEncode. Проверка связи с сервером выполняется известной нам функцией TerminalInfoInteger (см. раздел Проверка сетевых подключений).

Сервис не обязан знать, какие счета являются мастер-счетами, а какие дополнительными, — ему достаточно особым образом "подписывать" пары любых последовательно залогиненных счетов. А вот уже конкретная прикладная программа должна дополнить процесс проверки своей "лицензии": помимо сравнения текущего счета с мастер-счетом следует повторить алгоритм сервиса: составить пару [мастер-счет;текущий счет], подсчитать для неё зашифрованную "подпись" и проверить, есть ли она среди глобальных переменных.

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

В качестве дополнительных мер можно, например, шифровать в "подписи" время её создания и предусмотреть истечение прав каждые сутки (или с другой периодичностью). Другой вариант — генерация случайного числа при запуске сервиса и добавление его в подписываемую информацию наравне с номерами счетов. Это число известно только внутри сервиса, но он может транслировать его заинтересованным MQL-программам на графики с помощью функции EventChartCustom. Таким образом, "подпись" продолжит действовать в данном экземпляре терминала вплоть до завершения сеанса. В каждом сеансе будет генерироваться и рассылаться новое случайное число, поэтому оно не подойдет для других терминалов. Наконец, самым простым и удобным вариантом будет, вероятно, добавление в "подпись" времени старта системы: (TimeLocal() - GetTickCount() / 1000) или производного от него.

Из различных типов MQL-программ только некоторые продолжают выполняться между переключениями счетов и позволяют реализовать данную схему защиты. Поскольку требуется единообразным образом защитить MQL-программы любых типов, включая индикаторы и эксперты (которые перезагружаются при смене счета), имеет смысл поручить эту задачу сервису. Тогда сервис, постоянно работающий с момента загрузки терминала и до его закрытия, будет контролировать логины и генерировать уполномочивающие "подписи".

Исходный код сервиса приведен в файле MQL5/Services/MQL5Book/p5/ServiceAccount.mq5. Во входных параметрах задается мастер-счет и префикс глобальных переменных, в которых будут сохраняться "подписи". В реальных программах списки мастер-счетов должны быть зашиты в исходном коде, а вместо глобальных переменных лучше использовать файлы в папке Common, чтобы охватить также и тестер.

#property service
   
input long MasterAccount = 123456789;
input string Prefix = "!A_";

Главная функция сервиса выполняет свою работу следующим образом: в бесконечном цикле с паузами в 1 секунду отслеживаем смены счетов и сохраняем последний номер, создаем для пары "подпись" и записываем её в глобальную переменную. Созданием подписи занимается функция Cipher.

void OnStart()
{
   static long account = 0// предыдущий счет логина
   
   for(; !IsStopped(); )
   {
      // требуем связи, успешного логина и полного доступа (не инвесторского)
      const bool c = TerminalInfoInteger(TERMINAL_CONNECTED)
                  && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED);
      const long a = c ? AccountInfoInteger(ACCOUNT_LOGIN) : 0;
   
      if(account != a// счет сменился
      {
         if(a != 0// текущий счет
         {
            if(account != 0// предыдущий счет
            {
               // перенос авторизации с одного на другой
               const string signature = Cipher(accounta);
               PrintFormat("Account %I64d registered by %I64d: %s",
                  aaccountsignature);
               // сохраним запись о связи счетов
               if(StringLen(signature) > 0)
               {
                  GlobalVariableTemp(Prefix + signature);
                  GlobalVariableSet(Prefix + signatureaccount);
               }
            }
            else // первый счет авторизован, ждем второго 
            {
               PrintFormat("New account %I64d detected"a);
            }
            // запомним последний активный счет
            account = a;
         }
      }
      Sleep(1000);
   }
}

Функция Cipher использует специальное объединение ByteOverlay2, чтобы представить пару номеров счетов (типа long) в виде байтового массива, который передается для шифрования в CryptEncode (здесь выбран метод шифрования CRYPT_DES, но его можно заменить на CRYPT_AES128, CRYPT_AES256 или просто хеширование CRYPT_HASH_SHA256 (с секретом в качестве соли), если восстановление информации из подписи не требуется).

template<typename T>
union ByteOverlay2
{
   T values[2];
   uchar bytes[sizeof(T) * 2];
   ByteOverlay2(const T v1const T v2) { values[0] = v1values[1] = v2; }
};
   
string Cipher(const long data1const long data2)
{
   // TODO: замените секрет на свое кодовое слово
   // TODO: для методов CRYPT_AES128/CRYPT_AES256 нужны массивы 16/32 байта
   const static uchar secret[] = {'S', 'E', 'C', 'R', 'E', 'T', '0'};
   ByteOverlay2<longbo(data1data2);
   uchar result[];
   if(CryptEncode(CRYPT_DESbo.bytessecretresult) > 0)
   {
      uchar dummy[], text[];
      if(CryptEncode(CRYPT_BASE64resultdummytext) > 0)
      {
         return CharArrayToString(text);
      }
   }
   return NULL;
}

Далее любая программа в терминале может проверить, нет ли в глобальных переменных "лицензии" для текущего счета. Это делается с помощью функций CheckAccounts и IsCurrentAccountAuthorizedByMaster. Они приведены в сервисе просто для демонстрации.

Функция CheckAccounts выполняет проверку по всем мастер-счетам, зашитым в код, не совпадает ли один из них с текущим.

bool CheckAccounts()
{
   const long accounts[] = {MasterAccount}; // TODO: заполнить массив константами
   for(int i = 0i < ArraySize(accounts); ++i)
   {
      if(IsCurrentAccountAuthorizedByMaster(accounts[i])) return true;
   }
   return false;
}

IsCurrentAccountAuthorizedByMaster принимает в качестве параметра номер одного мастер-счета, воссоздает для него "подпись" в паре с текущим счетом и анализирует совпадения.

bool IsCurrentAccountAuthorizedByMaster(const long data)
{
   const long a = AccountInfoInteger(ACCOUNT_LOGIN);
   if(a == datareturn true// прямое совпадение
   const string s = Cipher(dataa); // пересчитываем "подпись"
   if(a != 0 && GlobalVariableGet(Prefix + s) == a)
   {
      Print("Sub-License is active: "s);
      return true;
   }
   return false;
}

Предположим, что программам разрешено выполняться на счете 123456789, и он в данный момент активен. При запуске сервис среагирует записью в журнале:

New account 123456789 detected

Если затем сменить номер счета, например, на 5555555, получим следующую сигнатуру:

Account 5555555 registered by 123456789: jdVKxUswBiNlZzDAnV3yxw==

Если остановить и снова запустить сервис, увидим проверку счета 5555555 в действии (вызов функции CheckAccounts встроен для демонстрации в начало OnStart).

Sub-License is active: jdVKxUswBiNlZzDAnV3yxw==
Account 123456789 registered by 5555555: ZWcwwJ1d8seN1UrFSzAGIw==

Лицензия сработала для нового счета. Если переключиться обратно, сгенерируется "пропуск" с текущего счета на предыдущий (это следствие того, что сервис не знает, какие счета являются основными, а какие временными, и такая "подпись", скорее всего, не потребуется в программах).

Для опосредованной авторизации нового счета потребуется снова войти в мастер-счет и только затем переключиться на новый: это создаст другую глобальную переменную с зашифрованной парой [мастер-счет; новый счет].

Данная версия сервиса не проверяет, чтобы мастер-счет был реальным, а зависимый — демонстрационным. Каждое из этих ограничений можно добавить.