Servicios

Un servicio es un programa MQL con un único manejador OnStart y la directiva #property service.

Recuerde que, tras la compilación correcta del servicio, debe crear y configurar su instancia (una o varias) mediante el comando Añadir servicio en el menú contextual de la ventana Navegador.

Como ejemplo de un servicio, vamos a resolver un pequeño problema práctico que suele surgir entre los desarrolladores de programas MQL. Muchos de ellos practican la vinculación de sus programas al número de cuenta del usuario. No se trata necesariamente de un producto de pago, sino que puede referirse a la distribución entre amigos y conocidos para recopilar estadísticas o configuraciones de éxito. Al mismo tiempo, el usuario puede registrar cuentas demo además de una cuenta real operativa. La vida útil de estas cuentas suele ser limitada, por lo que resulta bastante incómodo actualizar el enlace de las mismas cada dos semanas. Para ello, es necesario editar el código fuente, compilar y volver a enviar el programa.

En lugar de ello, podemos desarrollar un servicio que registre en variables globales (o archivos) los números de cuentas en las que se ha implementado una conexión con éxito desde el terminal dado.

La tecnología de vinculación se basa en el cifrado por pares (o, alternativamente, hashing) de los números de cuenta: la cuenta de inicio de sesión antigua y la cuenta de inicio de sesión nueva. La cuenta anterior debe ser una cuenta maestra (a la que se «emite» el enlace condicional) para que la firma común del par extienda los derechos de uso del producto a la nueva cuenta. La clave es un secreto conocido sólo dentro de los programas (se supone que todos ellos se suministran de forma cerrada y compilada). El resultado de la operación será una cadena con el formato Base64. La implementación utiliza funciones de la API de MQL5, algunas de las cuales aún están pendientes de estudio; en concreto, la obtención de un número de cuenta a través de la función de encriptación AccountInfoInteger y CryptEncode. La conexión con el servidor se comprueba mediante la función TerminalInfoInteger (véase Comprobación de las conexiones de red).

El servicio no está obligado a saber qué cuentas son principales y cuáles son adicionales. Sólo necesita «firmar» de manera especial pares de cuentas que hayan iniciado sesión sucesivamente. Pero un programa de aplicación específico debería complementar el proceso de comprobación de su «licencia»: además de comparar la cuenta corriente con la cuenta maestra, debería repetir el algoritmo de servicio creando un par [cuenta maestra; cuenta corriente], calculando la firma cifrada para él y comprobando si se encuentra entre las variables globales.

Será posible robar dicha licencia transfiriéndola a otro ordenador sólo si se conecta a la misma cuenta en modo de negociación (no inversor). Un usuario sin escrúpulos, por supuesto, puede crear cuentas demo para otras personas. Por lo tanto, es deseable mejorar la protección. En la implementación actual, la variable global simplemente se hace temporal, es decir, se borra junto con el final de la sesión de terminal, pero esto no impide su posible copia.

Como medidas adicionales, es posible, por ejemplo, cifrar la hora de su creación en la firma y prever la caducidad de los derechos cada día (o con otra frecuencia). Otra opción es generar un número aleatorio cuando se inicia el servicio y añadirlo a la información firmada junto con los números de cuenta. Este número sólo se conoce dentro del servicio, pero puede traducirlo a los programas MQL interesados en los gráficos mediante la función EventChartCustom. Así, la firma seguirá siendo válida en esta instancia del terminal hasta el final de la sesión. Cada sesión generará y enviará un nuevo número aleatorio, por lo que no funcionará para otros terminales. Por último, la opción más sencilla y cómoda sería probablemente añadir a la firma la hora de inicio del sistema: (TimeLocal() - GetTickCount() / 1000) o su derivado.

De los distintos tipos de programas MQL, sólo algunos siguen ejecutándose entre cambios de cuenta y permiten aplicar este esquema de protección. Dado que es necesario proteger de manera uniforme los programas MQL de todo tipo, incluidos los indicadores y los Asesores Expertos (que se recargan cuando se cambia la cuenta), tiene sentido confiar esta tarea a un servicio. A continuación, el servicio, que se ejecuta constantemente desde que se carga el terminal hasta que se cierra, controlará los inicios de sesión y generará firmas de autorización.

El código fuente del servicio figura en el archivo MQL5/Services/MQL5Book/p5/ServiceAccount.mq5. Los parámetros de entrada especifican la cuenta maestra y el prefijo de las variables globales en las que se almacenarán las firmas. En los programas reales, las listas de cuentas maestras deberían estar codificadas en el código fuente, y en lugar de variables globales, es mejor utilizar archivos en la carpeta Common para abarcar también el probador.

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

La función principal del servicio realiza su trabajo de la siguiente manera: en un bucle sin fin con pausas de 1 segundo, rastreamos los cambios de cuenta y guardamos el último número, creamos una firma para el par y la escribimos en una variable global. La firma se crea mediante la función Cipher.

void OnStart()
{
   static long account = 0// previous login
   
   for(; !IsStopped(); )
   {
      // require connection, successful login and full access (not investor)
      const bool c = TerminalInfoInteger(TERMINAL_CONNECTED)
                  && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED);
      const long a = c ? AccountInfoInteger(ACCOUNT_LOGIN) : 0;
   
      if(account != a// account changed
      {
         if(a != 0// current account
         {
            if(account != 0// previous account
            {
               // transfer authorization from one to another
               const string signature = Cipher(accounta);
               PrintFormat("Account %I64d registered by %I64d: %s"
                  aaccountsignature);
               // saving a record about the connection of accounts
               if(StringLen(signature) > 0)
               {
                  GlobalVariableTemp(Prefix + signature);
                  GlobalVariableSet(Prefix + signatureaccount);
               }
            }
            else // the first account is authorized, now waiting for the second one
            {
               PrintFormat("New account %I64d detected"a);
            }
            // remember the last active account
            account = a;
         }
      }
      Sleep(1000);
   }
}

La función Cipher utiliza una unión especial ByteOverlay2 para representar un par de números de cuenta (de tipo long) como un array de bytes, que se pasa para su cifrado en CryptEncode (aquí se elige el método de cifrado CRYPT_DES, pero puede sustituirse por el hashing CRYPT_AES128, CRYPT_AES256 o simplemente CRYPT_HASH_SHA256 (con el secreto como «sal»), si no se requiere la recuperación de información de la «firma»).

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: replace the secret with your passphrase
   // TODO: CRYPT_AES128/CRYPT_AES256 methods require 16/32 byte arrays
   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;
}

Entonces, cualquier programa en el terminal puede comprobar si hay «licencias» para la cuenta corriente en las variables globales. Para ello se utilizan las funciones CheckAccounts y IsCurrentAccountAuthorizedByMaster, que se muestran en el servicio sólo a efectos de demostración.

Las funciones CheckAccounts realizan una comprobación de todas las cuentas maestras codificadas para encontrar las que coinciden con la actual.

bool CheckAccounts()
{
   const long accounts[] = {MasterAccount}; // TODO: to fill array with constants
   for(int i = 0i < ArraySize(accounts); ++i)
   {
      if(IsCurrentAccountAuthorizedByMaster(accounts[i])) return true;
   }
   return false;
}

IsCurrentAccountAuthorizedByMaster toma el número de una cuenta maestra como parámetro, recrea una «firma» para ella en un par con la cuenta actual y analiza las coincidencias.

bool IsCurrentAccountAuthorizedByMaster(const long data)
{
   const long a = AccountInfoInteger(ACCOUNT_LOGIN);
   if(a == datareturn true// direct match
   const string s = Cipher(dataa); // recalculating "signature"
   if(a != 0 && GlobalVariableGet(Prefix + s) == a)
   {
      Print("Sub-License is active: "s);
      return true;
   }
   return false;
}

Supongamos que se permite la ejecución de programas en la cuenta 123456789 y que ésta actualmente está activa. Al iniciarse, el servicio responderá con una entrada de registro:

New account 123456789 detected

Si a continuación cambiamos el número de cuenta, por ejemplo, a 5555555, obtendremos la siguiente firma:

Account 5555555 registered by 123456789: jdVKxUswBiNlZzDAnV3yxw==

Si detenemos e iniciamos de nuevo el servicio, veremos la verificación de la cuenta 5555555 en acción (llamando a la función CheckAccounts incrustada para demostración al principio OnStart).

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

La licencia ha funcionado para la nueva cuenta. Si vuelve a cambiar, se generará un «pase» de la cuenta actual a la anterior (esto es consecuencia de que el servicio no «sabe» qué cuentas son principales y cuáles temporales, y lo más probable es que dicha «firma» no sea necesaria en los programas).

Para autorizar indirectamente una nueva cuenta, tendrá que volver a conectarse a la cuenta maestra y sólo entonces cambiar a la nueva: esto creará otra variable global con el par cifrado [cuenta maestra; nueva cuenta].

Esta versión del servicio no comprueba que la cuenta maestra sea real y la cuenta dependiente sea demo. Cada una de estas restricciones se puede añadir.