Intercambio de datos con un servidor web a través de HTTP/HTTPS

MQL5 permite integrar programas con servicios web y solicitar datos de Internet. Los datos pueden enviarse y recibirse a través de los protocolos HTTP/HTTPS utilizando la función WebRequest, que tiene dos versiones: una para una interacción simplificada y la otra para una avanzada con servidores web.

int WebRequest(const string method, const string url, const string cookie, const string referer,
int timeout, const char &data[], int size, char &result[], string &response)

int WebRequest(const string method, const string url, const string headers, int timeout,
const char &data[], char &result[], string &response)

La principal diferencia entre las dos funciones es que la versión simplificada sólo permite especificar dos tipos de encabezados en la petición: un cookie y un referer, es decir, la dirección desde la que se realiza la transición (aquí no hay ningún error tipográfico: históricamente, la palabra «referrer» se escribe en las cabeceras HTTP mediante una 'r'). La versión extendida toma un parámetro genérico headers para enviar un conjunto arbitrario de encabezados. Los encabezados de solicitud tienen la forma «nombre: valor» y van unidas por un salto de línea «\r\n» si hay más de una.

Si asumimos que la cadena cookie debe contener «nombre1=valor1; nombre2=valor2» y el enlace referer es igual a «google.com», entonces, para llamar a la segunda versión de la función con el mismo efecto que la primera, necesitamos añadir lo siguiente en el parámetro headers: «Cookie: nombre1=valor1; nombre2=valor2\r\nReferer: google.com».

El parámetro method especifica uno de los métodos de protocolo, «HEAD», «GET» o «POST». La dirección del recurso o servicio solicitado se pasa en el parámetro url. Según la especificación HTTP, la longitud de un identificador de recurso de red está limitada a 2048 bytes, pero en el momento de escribir el libro, MQL5 tenía un límite de 1024 bytes.

La duración máxima de una solicitud viene determinada por la dirección timeout en milisegundos.

Ambas versiones de la función transfieren datos del array data al servidor. La primera opción requiere además especificar el tamaño de este array en bytes (size).

Para enviar peticiones sencillas con valores de varias variables, puede combinarlas en una cadena como «nombre1=valor1&nombre2=valor2&...» y añadirlas a la dirección de la petición GET, después del carácter delimitador '?' o ponerlas en el array data para una petición POST utilizando el encabezado «Content-Type: application/x-www-form-urlencoded». Para casos más complejos, como la carga de archivos, utilice una solicitud POST y «Content-Type: multipart/form-data».

El array receptor result obtiene el cuerpo de la respuesta del servidor (si existe). Los encabezados de respuesta del servidor se colocan en la cadena response.

La función devuelve el código de respuesta HTTP del servidor, o -1 en caso de error del sistema (por ejemplo, problemas de comunicación o errores de parámetros). Los posibles códigos de error que pueden aparecer en _LastError incluyen:

  • 5200 - ERR_WEBREQUEST_INVALID_ADDRESS - URL no válida
  • 5201 - ERR_WEBREQUEST_CONNECT_FAILED - no se pudo conectar a la URL especificada
  • 5202 - ERR_WEBREQUEST_TIMEOUT - se ha superado el tiempo de espera para recibir una respuesta del servidor
  • 5203 - ERR_WEBREQUEST_REQUEST_FAILED - cualquier otro error como resultado de la solicitud

Recordemos que, aunque la solicitud se haya ejecutado sin errores en el nivel MQL5, el código de respuesta HTTP del servidor puede contener un error de aplicación (por ejemplo, se requiere autorización, formato de datos no válido, página no encontrada, etc.). En este caso, el resultado estará vacío, y las instrucciones para resolver la situación, por regla general, se aclaran analizando los encabezados response recibidos.

Para utilizar la función WebRequest, las direcciones del servidor deben añadirse a la lista de URL permitidas en la pestaña Expert Advisors de la configuración del terminal. El puerto del servidor se selecciona automáticamente en función del protocolo especificado: 80 para «http://" y 443 para "https://».

La función fWebRequest es sincrónica, es decir, detiene la ejecución del programa a la espera de una respuesta del servidor. A este respecto, no se permite llamar a la función desde los indicadores, ya que trabajan en flujos comunes para cada carácter. Un retraso en la ejecución de un indicador detendrá la actualización de todos los gráficos para este símbolo.

Cuando se trabaja en el probador de estrategias, la función WebRequest no se ejecuta.

Empecemos con un simple script WebRequestTest.mq5 que ejecuta una única petición. En los parámetros de entrada, proporcionaremos una opción para el método (por defecto «GET»), la dirección de la página web de prueba, encabezados adicionales (opcional), y también el tiempo de espera.

input string Method = "GET"// Method (GET,POST)
input string Address = "https://httpbin.org/headers";
input string Headers;
input int Timeout = 5000;

La dirección se introduce como en la línea del navegador: todos los caracteres que la especificación HTTP prohíbe utilizar directamente en las direcciones (incluidos los caracteres del alfabeto local) quedan «enmascarados» automáticamente por la función WebRequest antes de enviarlos según el algoritmo urlencode (el navegador hace exactamente lo mismo, pero nosotros no lo vemos, ya que esta vista está destinada a ser transmitida a través de la infraestructura de red, no a los humanos).

También añadiremos la opción DumpDataToFiles: cuando sea igual a true, el script guardará la respuesta del servidor en un archivo aparte, ya que puede ser bastante grande. El valor false indica que los datos se envíen directamente al registro.

input bool DumpDataToFiles = true;

Hay que decir de entrada que para probar este tipo de scripts se necesita un servidor. Los interesados pueden instalar un servidor web local, por ejemplo, node.js, pero esto requiere una preparación propia o la instalación de scripts del lado del servidor (en este caso, la conexión de módulos JavaScript). Una forma más sencilla es utilizar los servidores web públicos de prueba disponibles en Internet. Puede utilizar, por ejemplo, httpbin.org, httpbingo.org, webhook site, putsreq.com, www.mockable.io o reqbin.com. Ofrecen un conjunto diferente de prestaciones; elija o encuentre el más adecuado para usted (cómodo y comprensible, o lo más flexible posible).

En el parámetro Address, la dirección por defecto es el endpoint de la API del servidor httpbin.org. Esta «página web» dinámica devuelve al cliente los encabezados HTTP de su solicitud (en formato JSON). Así, podremos ver en nuestro programa qué es exactamente lo que ha llegado al servidor web desde el terminal.

No olvide añadir el dominio «httpbin.org» a la lista de permitidos en la configuración del terminal.

El formato de texto JSON es el estándar de facto para los servicios web. En el sitio mql5.com se pueden encontrar implementaciones ya hechas de clases para analizar JSON, pero por ahora nos limitaremos a mostrar el JSON «tal cual».

En el manejador OnStart, llamamos a WebRequest con los parámetros dados y procesamos el resultado si el código de error es no negativo. Los encabezados de respuesta del servidor (response) se registran siempre.

void OnStart()
{
   uchar data[], result[];
   string response;
   
   int code = PRTF(WebRequest(MethodAddressHeadersTimeoutdataresultresponse));
   if(code > -1)
   {
      Print(response);
      if(ArraySize(result) > 0)
      {
         PrintFormat("Got data: %d bytes"ArraySize(result));
         if(DumpDataToFiles)
         {
            string parts[];
            URL::parse(Addressparts);
            
            const string filename = parts[URL_HOST] +
               (StringLen(parts[URL_PATH]) > 1 ? parts[URL_PATH] : "/_index_.htm");
            Print("Saving "filename);
            PRTF(FileSave(filenameresult));
         }
         else
         {
            Print(CharArrayToString(result080CP_UTF8));
         }
      }
   }
}

Para formar el nombre del archivo, utilizamos la clase auxiliar URL del archivo de encabezado URL.mqh (que no se describirá completamente aquí). El método URL::parse analiza la cadena pasada en componentes de URL de acuerdo con la especificación, ya que la forma general de la URL es siempre «protocol://domain.com:port/path?query#hash»; tenga en cuenta que muchos fragmentos son opcionales. Los resultados se colocan en el array receptor, cuyos índices corresponden a partes específicas de la URL y se describen en la enumeración URL_PARTS:

enum URL_PARTS
{
   URL_COMPLETE,   // full address
   URL_SCHEME,     // protocol
   URL_USER,       // username/password (deprecated, not supported)
   URL_HOST,       // server
   URL_PORT,       // port number
   URL_PATH,       // path/directories
   URL_QUERY,      // query string after '?'
   URL_FRAGMENT,   // fragment after '#' (not highlighted)
   URL_ENUM_LENGTH
};

Así, cuando los datos recibidos deben escribirse en un archivo, el script lo crea en una carpeta con el nombre del servidor (parts[URL_HOST]) y así sucesivamente, conservando la jerarquía de rutas en la URL (parts[URL_PATH]): en el caso más sencillo, será simplemente el nombre del «endpoint». Cuando se solicita la página de inicio de un sitio (la ruta contiene únicamente una barra '/'), el archivo se denomina «_index_.htm».

Intentemos ejecutar el script con los parámetros predeterminados, recordando primero permitir este servidor en la configuración del terminal. En el registro veremos las siguientes líneas (encabezados HTTP de la respuesta del servidor y un mensaje sobre el guardado correcto del archivo):

WebRequest(Method,Address,Headers,Timeout,data,result,response)=200 / ok
Date: Fri, 22 Jul 2022 08:45:03 GMT
Content-Type: application/json
Content-Length: 291
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
   
Got data: 291 bytes
Saving httpbin.org/headers
FileSave(filename,result)=true / ok

El archivo httpbin.org/headers contiene los encabezados de nuestra petición tal y como las ve el servidor (el propio servidor ha añadido el formato JSON al respondernos).

{
  "headers":
  {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Accept-Language": "ru,en", 
    "Host": "httpbin.org", 
    "User-Agent": "MetaTrader 5 Terminal/5.3333 (Windows NT 10.0; Win64; x64)", 
    "X-Amzn-Trace-Id": "Root=1-62da638f-2554..." // <- this is added by the reverse proxy server
  }
}

Así, el terminal informa de que está preparado para aceptar datos de cualquier tipo, con compatibilidad para compresión por métodos específicos y una lista de idiomas preferidos. Además, aparece en el campo User-Agent como MetaTrader 5. Esto último puede resultar indeseable cuando se trabaja con algunos sitios optimizados para funcionar exclusivamente con navegadores. Entonces podemos especificar un nombre ficticio en el parámetro de entrada headers, por ejemplo, «User-Agent: Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36».

Algunos de los sitios de prueba mencionados anteriormente le permiten organizar un entorno de prueba temporal en el servidor con un nombre aleatorio para su experimento personal: para ello, tiene que ir al sitio desde un navegador y obtener un enlace único que suele funcionar durante 24 horas. Entonces podrá utilizar este enlace como dirección para las solicitudes de MQL5 y monitorizar el comportamiento de las mismas directamente desde el navegador. Allí también puede configurar las respuestas del servidor, en particular, el intento de envío de formularios.

Hagamos este ejemplo un poco más difícil. El servidor puede requerir acciones adicionales del cliente para dar respuesta a la solicitud; en concreto, autorizar, realizar una «redirección» (ir a una dirección diferente), reducir la frecuencia de las solicitudes, etc. Todas estas «señales» se denotan mediante códigos HTTP especiales devueltos por la función WebRequest. Por ejemplo, los códigos 301 y 302 significan redirigir por motivos diferentes, y WebRequest lo ejecuta internamente de forma automática, volviendo a solicitar la página en la dirección especificada por el servidor (por lo tanto, los códigos de redirección nunca acaban en el código del programa MQL). El código 401 requiere que el cliente proporcione un nombre de usuario y una contraseña, y aquí toda la responsabilidad recae en nosotros. Hay muchas formas de enviar estos datos. Un nuevo script WebRequestAuth.mq5 demuestra el manejo de dos opciones de autorización que el servidor solicita utilizando encabezados de respuesta HTTP: «WWW-Authenticate: Basic» o «WWW-Authenticate: Digest». En los encabezados podría tener este aspecto:

WWW-Authenticate:Basic realm="DemoBasicAuth"

O este:

WWW-Authenticate:Digest realm="DemoDigestAuth",qop="auth", »
»  nonce="cuFAuHbb5UDvtFGkZEb2mNxjqEG/DjDr",opaque="fyNjGC4x8Zgt830PpzbXRvoqExsZeQSDZj"

La primera de ellas es la más sencilla y menos segura, por lo que prácticamente no se utiliza: se ofrece en el libro por lo fácil que es aprenderla en una primera fase. El resultado final de su trabajo es generar la siguiente petición HTTP en respuesta a una solicitud del servidor añadiendo un encabezado especial:

Authorization: Basic dXNlcjpwYXNzd29yZA==

Aquí, la palabra clave «Basic» va seguida de la cadena codificada en Base64 «user:password» con el nombre de usuario y la contraseña reales, y el carácter ':' se inserta a continuación «tal cual» como bloque de enlace. En la imagen se muestra más claramente el proceso de interacción:

Esquema sencillo de autorización en un servidor web

Esquema sencillo de autorización en un servidor web

El esquema de autorización Digest se considera más avanzado. En este caso, el servidor proporciona información adicional en su respuesta:

  • realms - el nombre del sitio (zona del sitio) en el que se realiza la entrada
  • qop - una variación del método Digest (sólo consideraremos «auth»)
  • nonce - una cadena aleatoria que se utilizará para generar los datos de autorización
  • opaque - una cadena aleatoria que devolveremos «tal cual» en nuestros encabezados
  • algorithm - un nombre opcional del algoritmo de hashing, MD5 se asume de manera predeterminada

Para la autorización, debe realizar los siguientes pasos:

  1. Genere su propia cadena aleatoria cnonce
  2. Inicialice o incremente su contador de solicitudes nc
  3. Calcule hash1 = MD5(user:realm:password)
  4. Calcule hash2 = MD5(method:uri), aquí uri es la ruta y el nombre de la página
  5. Calcule response = MD5(hash1:nonce:nc:cnonce:qop:hash2)

Después, el cliente puede repetir la solicitud al servidor, añadiendo una línea como ésta a sus encabezados:

Authorization: Digest username="user",realm="realm",nonce="...", »
»  uri="/path/to/page",qop=auth,nc=00000001,cnonce="...",response="...",opaque="..."

Como el servidor tiene la misma información que el cliente, podrá repetir los cálculos y comprobar que los hashes coinciden.

Añadamos variables a los parámetros del script para introducir el nombre de usuario y la contraseña. De manera predeterminada, el parámetro Address incluye la dirección del endpoint digest-auth, que puede solicitar autorización con los parámetros qop («auth»), login («test») y password («pass»). Todo esto es opcional en la ruta del endpoint (puede probar otros métodos y credenciales de usuario, como por ejemplo así: «https://httpbin.org/digest-auth/auth-int/mql5client/mql5password»).

const string Method = "GET";
input string Address = "https://httpbin.org/digest-auth/auth/test/pass";
input string Headers = "User-Agent: noname";
input int Timeout = 5000;
input string User = "test";
input string Password = "pass";
input bool DumpDataToFiles = true;

Hemos especificado un nombre de navegador ficticio en el parámetro Headers para demostrar la función.

En la función OnStart, añadimos el procesamiento del código HTTP 401. Si no se proporciona un nombre de usuario y una contraseña, no podremos continuar.

void OnStart()
{
   string parts[];
   URL::parse(Addressparts);
   uchar data[], result[];
   string response;
   int code = PRTF(WebRequest(MethodAddressHeadersTimeoutdataresultresponse));
   Print(response);
   if(code == 401)
   {
      if(StringLen(User) == 0 || StringLen(Password) == 0)
      {
         Print("Credentials required");
         return;
      }
      ...

El siguiente paso es analizar los encabezados recibidos del servidor. Por comodidad, hemos escrito la clase HttpHeader (HttpHeader.mqh). El texto completo se pasa a su constructor, así como el separador de elementos (en este caso, el carácter de nueva línea '\n') y el carácter utilizado entre el nombre y el valor dentro de cada elemento (en este caso, los dos puntos ':'). Durante su creación, el objeto «analiza» el texto, y luego los elementos se ponen a disposición a través del operador sobrecargado [], siendo el tipo de su argumento una cadena. Como resultado, podemos comprobar si existe un requisito de autorización con el nombre «WWW-Authenticate». Si tal elemento existe en el texto y es igual a «Basic», formamos el encabezado de respuesta «Authorization: Basic» con el nombre de usuario y la contraseña codificados en Base64.

      code = -1;
      HttpHeader header(response, '\n', ':');
      const string auth = header["WWW-Authenticate"];
      if(StringFind(auth"Basic ") == 0)
      {
         string Header = Headers;
         if(StringLen(Header) > 0Header += "\r\n";
         Header += "Authorization: Basic ";
         Header += HttpHeader::hash(User + ":" + PasswordCRYPT_BASE64);
         PRTF(Header);
         code = PRTF(WebRequest(MethodAddressHeaderTimeoutdataresultresponse));
         Print(response);
      }
      ...

Para la autorización Digest, todo es un poco más complicado, siguiendo el algoritmo descrito anteriormente.

      else if(StringFind(auth"Digest ") == 0)
      {
         HttpHeader params(StringSubstr(auth7), ',', '=');
         string realm = HttpHeader::unquote(params["realm"]);
         if(realm != NULL)
         {
            string qop = HttpHeader::unquote(params["qop"]);
            if(qop == "auth")
            {
               string h1 = HttpHeader::hash(User + ":" + realm + ":" + Password);
               string h2 = HttpHeader::hash(Method + ":" + parts[URL_PATH]);
               string nonce = HttpHeader::unquote(params["nonce"]);
               string counter = StringFormat("%08x"1);
               string cnonce = StringFormat("%08x"MathRand());
               string h3 = HttpHeader::hash(h1 + ":" + nonce + ":" + counter + ":" +
                  cnonce + ":" + qop + ":" + h2);
               
               string Header = Headers;
               if(StringLen(Header) > 0Header += "\r\n";
               Header += "Authorization: Digest ";
               Header += "username=\"" + User + "\",";
               Header += "realm=\"" + realm + "\",";
               Header += "nonce=\"" + nonce + "\",";
               Header += "uri=\"" + parts[URL_PATH] + "\",";
               Header += "qop=" + qop + ",";
               Header += "nc=" + counter + ",";
               Header += "cnonce=\"" + cnonce + "\",";
               Header += "response=\"" + h3 + "\",";
               Header += "opaque=" + params["opaque"] + "";
               PRTF(Header);
               code = PRTF(WebRequest(Method, Address, Header, Timeout, data, result, response));
               Print(response);
            }
         }
      }

El método estático HttpHeader::hash obtiene una cadena con una representación hash hexadecimal (por defecto MD5) para todas las cadenas compuestas requeridas. A partir de estos datos se forma el encabezado para la siguiente llamada a WebRequest. El método estático HttpHeader::unquote elimina las comillas.

El resto del script se mantuvo sin cambios. Una petición HTTP repetida puede tener éxito, y entonces obtendremos el contenido de la página segura, o se denegará la autorización, y el servidor escribirá algo como «Acceso denegado».

Dado que los parámetros predeterminados contienen los valores correctos («/digest-auth/auth/test/pass» corresponde al usuario «test» y a la contraseña «pass»), deberíamos obtener el siguiente resultado al ejecutar el script (se registran todos los pasos y datos principales).

WebRequest(Method,Address,Headers,Timeout,data,result,response)=401 / ok
Date: Fri, 22 Jul 2022 10:45:56 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Connection: keep-alive
Server: gunicorn/19.9.0
WWW-Authenticate: Digest realm="me@kennethreitz.com" »
»  nonce="87d28b529a7a8797f6c3b81845400370", qop="auth",
»  opaque="4cb97ad7ea915a6d24cf1ccbf6feeaba", algorithm=MD5, stale=FALSE
...

La primera llamada a WebRequest ha finalizado con el código 401, y entre los encabezados de respuesta hay una solicitud de autorización («WWW-Authenticate») con los parámetros requeridos. A partir de ellos, calculamos la respuesta correcta y preparamos los encabezados para una nueva solicitud.

Header=User-Agent: noname
Authorization: Digest username="test",realm="me@kennethreitz.com" »
»  nonce="87d28b529a7a8797f6c3b81845400370",uri="/digest-auth/auth/test/pass",
»  qop=auth,nc=00000001,cnonce="00001c74",
»  response="c09e52bca9cc90caf9a707d046b567b2",opaque="4cb97ad7ea915a6d24cf1ccbf6feeaba" / ok
...

La segunda solicitud devuelve 200 y una carga útil que escribimos en el archivo.

WebRequest(Method,Address,Header,Timeout,data,result,response)=200 / ok
Date: Fri, 22 Jul 2022 10:45:56 GMT
Content-Type: application/json
Content-Length: 47
Connection: keep-alive
Server: gunicorn/19.9.0
...
Got data: 47 bytes
Saving httpbin.org/digest-auth/auth/test/pass
FileSave(filename,result)=true / ok

Dentro del archivo MQL5/Files/httpbin.org/digest-auth/auth/test/pass se encuentra la «página web», o mejor dicho, el estado de la autorización correcta en formato JSON.

{
  "authenticated": true, 
  "user": "test"
}

Si especifica una contraseña incorrecta al ejecutar el script, recibiremos una respuesta vacía del servidor y el archivo no se escribirá.

Utilizando WebRequest entramos automáticamente en el campo de los sistemas de software distribuido, en los que el correcto funcionamiento depende no sólo de nuestro código MQL cliente, sino también del servidor (por no hablar de los enlaces intermedios, como un proxy). Por lo tanto, hay que estar preparado para que se produzcan errores ajenos. En concreto, en el momento de escribir el libro en la implementación del endpoint digest-auth en httpbin.org, había un problema: el nombre de usuario introducido en la solicitud no participaba en la comprobación de autorización, por lo que cualquier inicio de sesión conducía a una autorización correcta si se especificaba la contraseña correcta. Aun así, para comprobar nuestro script, utilice otros servicios, por ejemplo, algo como httpbingo.org/digest-auth/auth/test/pass. También puede configurar el script a la dirección jigsaw.w3.org/HTTP/Digest/ - espera el login/contraseña «guest»/«invitado».

En la práctica, la mayoría de los sitios implementan la autorización utilizando formularios incrustados directamente en las páginas web: dentro del código HTML, son esencialmente la etiqueta contenedora form con un conjunto de campos de entrada, que son rellenados por el usuario y enviados al servidor utilizando el método POST. A este respecto, tiene sentido analizar el ejemplo del envío de un formulario. Sin embargo, antes de entrar en detalles, conviene destacar una técnica más.

La cuestión es que la interacción entre el cliente y el servidor suele ir acompañada de un cambio en el estado tanto del cliente como del servidor. Utilizando el ejemplo de la autorización, esto se puede entender más claramente, ya que antes de la autorización el usuario era desconocido para el sistema, y después de eso, el sistema ya conoce el inicio de sesión y puede aplicar la configuración preferida para el sitio (por ejemplo, idioma, color, método de visualización del foro), y también permitir el acceso a aquellas páginas en las que los visitantes no autorizados no pueden entrar (el servidor detiene tales intentos devolviendo el estado HTTP 403, Prohibido).

La compatibilidad y la sincronización del estado consistente de las partes cliente y servidor de una aplicación web distribuida se proporciona utilizando el mecanismo de cookies que implica variables con nombre y sus valores en los encabezados HTTP. El término se remonta a las «galletas de la suerte», porque cookies también contiene pequeños mensajes invisibles para el usuario.

Cualquiera de las partes, servidor y cliente, puede añadir cookie al encabezado HTTP. El servidor hace esto con una línea como esta:

Set-Cookiename=value; ⌠Domain=domainPath=pathExpires=dateMax-Age=number_of_seconds ...⌡ᵒᵖᵗ

Solo el nombre y el valor son obligatorios y el resto de los atributos son opcionales; los principales son Domain, Path, Expires y Max age, pero en situaciones reales hay más.

Una vez recibida dicho encabezado (o varios encabezados), el cliente debe recordar el nombre y el valor de la variable y enviarlos al servidor en todas las solicitudes que se dirijan a los correspondientes Domain y Path dentro de este dominio hasta la fecha de vencimiento (Expires o Max-Age).

En una solicitud HTTP saliente de un cliente, las cookies se pasan como una cadena:

Cookiename⁽№⁾=value⁽№⁾ ⌠; name⁽ⁱ⁾=value⁽ⁱ⁾ ...⌡ᵒᵖᵗ

Aquí, separados por un punto y coma y un espacio, se enumeran todos los pares nombre=valor; son establecidos por el servidor y conocidos por este cliente, coinciden con la solicitud actual por el dominio y la ruta, y no han vencido.

El servidor y el cliente intercambian todas las cookies necesarias con cada solicitud HTTP, por lo que este estilo arquitectónico de sistemas distribuidos se denomina REST (Representational State Transfer). Por ejemplo, después de que un usuario se conecte con éxito al servidor, éste establece (a través del encabezado «Set-Cookie:») una «cookie» especial con el identificador del usuario, tras lo cual el navegador web (o, en nuestro caso, un terminal con un programa MQL) la enviará en solicitudes posteriores (añadiendo la línea adecuada al encabezado «Cookie:»).

La función WebRequest hace silenciosamente todo este trabajo por nosotros: recoge las cookies de los encabezados entrantes y añade las cookies apropiadas a las solicitudes HTTP salientes.

Las cookies son almacenadas por el terminal y entre sesiones, según su configuración. Para comprobarlo, basta con solicitar dos veces una página web a un sitio que utilice cookies.

Atención: las cookies se almacenan en relación con el sitio y por lo tanto se sustituyen imperceptiblemente en los encabezados salientes de todos los programas MQL que utilizan WebRequest para el mismo sitio.

Para simplificar las solicitudes secuenciales, tiene sentido formalizar las acciones populares en una clase especial HTTPRequest (HTTPRequest.mqh). En ella almacenaremos encabezados HTTP comunes, que probablemente serán necesarios para todas las solicitudes (por ejemplo, idiomas admitidos, instrucciones para proxies, etc.). Además, también es común un ajuste como el tiempo de espera. Ambas opciones se pasan al constructor del objeto.

class HTTPRequestpublic HttpCookie
{
protected:
   string common_headers;
   int timeout;
   
public:
   HTTPRequest(const string hconst int t = 5000):
      common_headers(h), timeout(t) { }
   ...

De manera predeterminada, el tiempo de espera está fijado en 5 segundos. El principal método, en cierto sentido universal, de la clase es request.

   int request(const string methodconst string address,
      string headersconst uchar &data[], uchar &result[], string &response)
   {
      if(headers == NULLheaders = common_headers;
      
      ArrayResize(result0);
      response = NULL;
      Print(">>> Request:\n"method + " " + address + "\n" + headers);
      
      const int code = PRTF(WebRequest(methodaddressheaderstimeoutdataresultresponse));
      Print("<<< Response:\n"response);
      return code;
   }
};

Vamos a describir un par de métodos más para consultas de tipos específicos.

Las solicitudes GET sólo utilizan encabezados y el cuerpo del documento (a menudo se utiliza el término payload) está vacío.

   int GET(const string addressuchar &result[], string &response,
      const string custom_headers = NULL)
   {
      uchar nodata[];
      return request("GET"addresscustom_headersnodataresultresponse);
   }

En las solicitudes POST suele haber una carga útil.

   int POST(const string addressconst uchar &payload[],
      uchar &result[], string &responseconst string custom_headers = NULL)
   {
      return request("POST"addresscustom_headerspayloadresultresponse);
   }

Los formularios pueden enviarse en distintos formatos. El más sencillo es «application/x-www-form-urlencoded». Implica que la carga útil será una cadena (quizá muy larga, ya que las especificaciones no imponen restricciones, y todo depende de la configuración de los servidores web). Para tales formularios, proporcionaremos una sobrecarga más conveniente del método POST con el parámetro de cadena de carga útil.

   int POST(const string addressconst string payload,
      uchar &result[], string &responseconst string custom_headers = NULL)
   {
      uchar bytes[];
      const int n = StringToCharArray(payloadbytes0, -1CP_UTF8);
      ArrayResize(bytesn - 1); // remove terminal zero
      return request("POST"addresscustom_headersbytesresultresponse);
   }

Escribamos un sencillo script para probar el motor web de nuestro cliente WebRequestCookie.mq5. Su tarea consistirá en solicitar la misma página web dos veces: la primera vez el servidor ofrecerá con toda probabilidad instalar sus cookies, y luego serán sustituidas automáticamente en la segunda solicitud. En los parámetros de entrada, especifique la dirección de la página para la prueba: deje que sea el sitio web mql5.com. También simularemos los encabezados por defecto mediante la cadena «User-Agent» corregida.

input string Address = "https://www.mql5.com";
input string Headers = "User-Agent: Mozilla/5.0 (Windows NT 10.0) Chrome/103.0.0.0"// Headers (use '|' as separator, if many specified)

En la función principal del script, describimos el objeto HTTPRequest y ejecutamos dos peticiones GET en un bucle.

¡Atención! Esta prueba funciona bajo el supuesto de que los programas MQL aún no han visitado el sitio www.mql5.com y no han recibido cookies del mismo. Después de ejecutar el script una vez, las cookies permanecerán en la caché del terminal, y será imposible reproducir el ejemplo: en ambas iteraciones del bucle, obtendremos las mismas entradas de registro.
 
No olvide añadir el dominio «www.mql5.com» a la lista de permitidos en la configuración del terminal.

void OnStart()
{
   uchar result[];
   string response;
   HTTPRequest http(Headers);
   
   for(int i = 0i < 2; ++i)
   {
      if(http.GET(Addressresultresponse) > -1)
      {
         if(ArraySize(result) > 0)
         {
            PrintFormat("Got data: %d bytes"ArraySize(result));
            if(i == 0// show the beginning of the document only the first time
            {
               const string s = CharArrayToString(result0160CP_UTF8);
               int j = -1k = -1;
               while((j = StringFind(s"\r\n"j + 1)) != -1k = j;
               Print(StringSubstr(s0k));
            }
         }
      }
   }
}

La primera iteración del bucle generará las siguientes entradas de registro (con abreviaturas):

>>> Request:
GET https://www.mql5.com
User-Agent: Mozilla/5.0 (Windows NT 10.0) Chrome/103.0.0.0
WebRequest(method,address,headers,timeout,data,result,response)=200 / ok
<<< Response:
Server: nginx
Date: Sun, 24 Jul 2022 19:04:35 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: no-cache,no-store
Content-Encoding: gzip
Expires: -1
Pragma: no-cache
Set-Cookie: sid=CfDJ8O2AwC...Ne2yP5QXpPKA2; domain=.mql5.com; path=/; samesite=lax; httponly
Vary: Accept-Encoding
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self' ... 
Generate-Time: 2823
Agent-Type: desktop-ru-en
X-Cache-Status: MISS
Got data: 184396 bytes
   
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />

Hemos recibido una nueva cookie con el nombre sid. Para comprobar su eficacia, se pasa a ver la segunda parte del registro, para la segunda iteración del bucle.

>>> Request:
GET https://www.mql5.com
User-Agent: Mozilla/5.0 (Windows NT 10.0) Chrome/103.0.0.0
WebRequest(method,address,headers,timeout,data,result,response)=200 / ok
<<< Response:
Server: nginx
Date: Sun, 24 Jul 2022 19:04:36 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: no-cache, no-store, must-revalidate, no-transform
Content-Encoding: gzip
Expires: -1
Pragma: no-cache
Vary: Accept-Encoding
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self' ... 
Generate-Time: 2950
Agent-Type: desktop-ru-en
X-Cache-Status: MISS

Por desgracia, aquí no vemos los encabezados salientes completos formados dentro de WebRequest, pero la instancia de la cookie que se está enviando al servidor usando el encabezado «Cookie:» está probada por el hecho de que el servidor en su segunda respuesta ya no pide establecerla.

En teoría, esta cookie simplemente identifica al visitante (como hacen la mayoría de los sitios) pero no significa su autorización. Por lo tanto, volvamos al ejercicio de enviar el formulario de forma general, es decir, en el futuro la tarea privada de introducir un nombre de usuario y una contraseña.

Recordemos que para enviar el formulario podemos utilizar el método POST con un parámetro de cadena payload. El principio de la preparación de datos según la norma «x-www-form-urlencoded» es que las variables con nombre y sus valores se escriben en una línea continua (algo parecido a las cookies).

name⁽№⁾=value⁽№⁾[&name⁽ⁱ⁾=value⁽ⁱ⁾...]ᵒᵖᵗ

El nombre y el valor se conectan con el signo '=', y los pares se unen mediante el carácter ampersand '&'. Puede que falte el valor. Por ejemplo:

Name=John&Age=33&Education=&Address=

Es importante tener en cuenta que, desde un punto de vista técnico, esta cadena debe convertirse según el algoritmo antes de enviar urlencode (de ahí viene el nombre del formato); sin embargo, WebRequest hace esta transformación por nosotros.

Los nombres de las variables vienen determinados por el formulario web (el contenido de la etiqueta form en una página web) o la lógica de la aplicación web; en cualquier caso, el servidor web debe ser capaz de interpretar los nombres y valores. Por lo tanto, con el fin de familiarizarnos con la tecnología, necesitamos un servidor de prueba con un formulario.

El formulario de la prueba está disponible en https://httpbin.org/forms/post. Se trata de un diálogo para pedir una pizza.

Formulario web de prueba

Formulario web de prueba

Su estructura interna y su comportamiento se describen en el código HTML que aparece más abajo. En él, nos interesan principalmente las etiquetas input, que establecen las variables esperadas por el servidor. Además, hay que prestar atención al atributo action de la etiqueta form, ya que define la dirección a la que debe enviarse la petición POST, y en este caso es «/post», que junto con el dominio da la cadena «httpbin.org/post». Esto es lo que utilizaremos en el programa MQL.

<!DOCTYPE html>
<html>
  <body>
  <form method="post" action="/post">
    <p><label>Customer name: <input name="custname"></label></p>
    <p><label>Telephone: <input type=tel name="custtel"></label></p>
    <p><label>E-mail address: <input type=email name="custemail"></label></p>
    <fieldset>
      <legend> Pizza Size </legend>
      <p><label> <input type=radio name=size value="small"> Small </label></p>
      <p><label> <input type=radio name=size value="medium"> Medium </label></p>
      <p><label> <input type=radio name=size value="large"> Large </label></p>
    </fieldset>
    <fieldset>
      <legend> Pizza Toppings </legend>
      <p><label> <input type=checkbox name="topping" value="bacon"> Bacon </label></p>
      <p><label> <input type=checkbox name="topping" value="cheese"> Extra Cheese </label></p>
      <p><label> <input type=checkbox name="topping" value="onion"> Onion </label></p>
      <p><label> <input type=checkbox name="topping" value="mushroom"> Mushroom </label></p>
    </fieldset>
    <p><label>Preferred delivery time: <input type=time min="11:00" max="21:00" step="900" name="delivery"></label></p>
    <p><label>Delivery instructions: <textarea name="comments"></textarea></label></p>
    <p><button>Submit order</button></p>
  </form>
  </body>
</html>

En el script WebRequestForm.mq5 hemos preparado variables de entrada similares para que sean especificadas por el usuario antes de ser enviadas al servidor.

input string Address = "https://httpbin.org/post";
   
input string Customer = "custname=Vincent Silver";
input string Telephone = "custtel=123-123-123";
input string Email = "custemail=email@address.org";
input string PizzaSize = "size=small"// PizzaSize (small,medium,large)
input string PizzaTopping = "topping=bacon"// PizzaTopping (bacon,cheese,onion,mushroom)
input string DeliveryTime = "delivery=";
input string Comments = "comments=";

Las cadenas ya configuradas se muestran sólo para probarlas con un clic: puede sustituirlas por las suyas propias, pero tenga en cuenta que dentro de cada cadena sólo debe editarse el valor a la derecha de '=', y debe conservarse el nombre a la izquierda de '=' (los nombres desconocidos serán ignorados por el servidor).

En la función OnStart, describimos el encabezado HTTP «Content-Type:» y preparamos una cadena concatenada con todas las variables.

void OnStart()
{
   uchar result[];
   string response;
   string header = "Content-Type: application/x-www-form-urlencoded";
   string form_fields;
   StringConcatenate(form_fields,
      Customer"&",
      Telephone"&",
      Email"&",
      PizzaSize"&",
      PizzaTopping"&",
      DeliveryTime"&",
      Comments);
   HTTPRequest http;
   if(http.POST(Addressform_fieldsresultresponse) > -1)
   {
      if(ArraySize(result) > 0)
      {
         PrintFormat("Got data: %d bytes"ArraySize(result));
         // NB: UTF-8 is implied for many content-types,
 // but some may be different, analyze the response headers
         Print(CharArrayToString(result0WHOLE_ARRAYCP_UTF8));
      }
   }
}

A continuación ejecutamos el método POST y registramos la respuesta del servidor. He aquí un ejemplo de resultado:

>>> Request:
POST https://httpbin.org/post
Content-Type: application/x-www-form-urlencoded
WebRequest(method,address,headers,timeout,data,result,response)=200 / ok
<<< Response:
Date: Mon, 25 Jul 2022 08:41:41 GMT
Content-Type: application/json
Content-Length: 780
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
   
Got data: 721 bytes
{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {
    "comments": "", 
    "custemail": "email@address.org", 
    "custname": "Vincent Silver", 
    "custtel": "123-123-123", 
    "delivery": "", 
    "size": "small", 
    "topping": "bacon"
  }, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Accept-Language": "ru,en", 
    "Content-Length": "127", 
    "Content-Type": "application/x-www-form-urlencoded", 
    "Host": "httpbin.org", 
    "User-Agent": "MetaTrader 5 Terminal/5.3333 (Windows NT 10.0; x64)", 
    "X-Amzn-Trace-Id": "Root=1-62de5745-25bd1d823a9609f01cff04ad"
  }, 
  "json": null, 
  "url": "https://httpbin.org/post"
}

El servidor de prueba acusa recibo de los datos como copia JSON. En la práctica, el servidor, por supuesto, no devolverá los datos en sí, sino que simplemente informará de un estado de éxito y posiblemente redirigirá a otra página web en la que los datos hayan tenido efecto (por ejemplo, mostrar el número del pedido).

Con la ayuda de este tipo de solicitudes POST, pero de menor tamaño, se suele realizar también la autorización. Sin embargo, a decir verdad, la mayoría de los servicios web complican este proceso en exceso de forma deliberada por motivos de seguridad y obligan a calcular primero varias sumas hash a partir de los datos del usuario. Las API públicas especialmente desarrolladas suelen tener descripciones de todos los algoritmos necesarios en la documentación, pero no siempre es así. En concreto, no podremos conectarnos utilizando WebRequest en mql5.com porque el sitio no tiene una interfaz de programación abierta.

Cuando envíe solicitudes a servicios web, respete siempre la norma de no exceder la frecuencia de las solicitudes: normalmente, cada servicio especifica sus propios límites, y la violación de los mismos conllevará el consiguiente bloqueo de su programa cliente, cuenta o dirección IP.