Ir al contenido principal

Analizando el protocolo HTTP

El objetivo de este artículo es el de explicar de forma práctica el funcionamiento del protocolo HTTP y entender el intercambio de datos que se realiza entre los servidores y los clientes web. Por otro lado, cubre la necesidad de tener un texto en español que sirva de referencia a mis alumnos de Servicios en Red a la hora de realizar la práctica de clase HTTP-1.

La idea es ver de forma práctica el funcionamiento interno del protocolo HTTP. Para ello, vamos a utilizar un par de herramientas de la línea de comandos de Linux (telnet y netcat), con las que vamos a simular el comportamiento tanto del navegador como del servidor web.

HTTP es un protocolo de la capa de aplicación, y como muchos otros protocolos de esta capa, está basado en texto. De hecho, los comandos que envía el navegador al servidor y sus respuestas se pueden leer perfectamente en inglés.

Por defecto, HTTP utiliza el puerto 80 TCP y HTTPS el puerto 443 TCP. Los ejemplos que vamos a ilustrar serán sólo de HTTP.

Versiones de HTTP

La versión HTTP/1.1 es la más utilizada actualmente. Aunque podemos encontrar todavía servidores que utilizan la versión HTTP/1.0. A efectos prácticos, la versión HTTP/1.1 admite todos los comandos HTTP que vamos a comentar, mientras que la versión HTTP/1.0, sólo admite los tres primeros (GET, HEAD y POST).

Por otro lado, HTTTP/1.1 mantiene la conexión abierta por defecto, lo que permite que los clientes puedan interactuar múltiples veces con el servidor utilizando la misma conexión. En las versiones anteriores del protocolo, cada vez que el cliente necesita obtener un recurso, tiene que abrir una conexión nueva, resultando el proceso mucho más lento.

Hay que tener en cuenta que si el navegador se descarga una página web con varias imágenes, las imágenes no las envía el servidor junto con el código HTML de la página en sí. Es el navegador, una vez descargada la página HTML y tras su procesamiento, el que tiene que solicitarle al servidor el resto de los recursos que componen dicha página, realizando una petición nueva por cada recurso.

Solicitud y Respuesta HTTP

Cuando un cliente necesita solicitar algún recurso a un servidor web, lo hace mandándole un comando. El comando va acompañado de la versión del protocolo utilizado así como de algunos campos extras. El servidor procesa este comando y responde enviando una respuesta compuesta de una cabecera y de un cuerpo. La cabecera incorpora metainformación y el cuerpo el recurso solicitado. La separación de la cabecera y el cuerpo se establece por medio de un salto de línea.

La cabecera siempre está compuesta por la versión del protocolo que se está utilizando seguida de un código de estado formado por tres dígitos y una descripción textual del mismo. Este código permite al cliente saber si el comando ha podido llevarse a cabo en el servidor de forma correcta o si se ha producido algún error o ha ocurrido alguna eventualidad. Además, el comando puede ir acompañado de una serie de campos, formados por pares variable:valor, que aportan información adicional al servidor.

Por otro lado, si el comando altera el estado del servidor sólo se ejecutará si el cliente que lo envía cuenta con los permisos correspondientes.

Códigos de estado HTTP

Los códigos de estado están compuesto por tres dígitos, el primero determina la categoría del estado y los dos siguientes el tipo de estado dentro de esta categoría. Las categorías son las siguientes:
  • 1xx: Respuesta informativa.
  • 2xx: Acción realizada de forma correcta.
  • 3xx: El recurso se ha trasladado a otra ubicación.
  • 4xx: El cliente ha producido un error.
  • 5xx: Se ha producido un error en el servidor.

Comandos HTTP

El protocolo define una serie de comandos para permitir la comunicación del cliente con el servidor. El cliente, en la mayoría de los casos, es el navegador web. Últimamente se está desarrollando aplicaciones RESTful que también utilizan estos comandos para comunicarse con los servidores.

Los principales comandos HTTP son:
  • GET: Se utiliza para solicitar un recurso al servidor.
  • HEAD: Similar a GET, sólo que el servidor sólo devuelve la cabecera de la petición.
  • POST: Permite el envío de datos a una aplicación del servidor. También sirve para crear objetos en el caso de aplicaciones RESTful.
  • PUT: Permite el envío de datos al servidor. Normalmente se utiliza para subir ficheros.
  • DELETE: Permite el borrado de recursos del servidor.

Simulando un cliente HTTP 

Vamos a utilizar el programa telnet para conectarnos a un servidor web y simular el comportamiento de un navegador. Al mismo tiempo veremos cómo el servidor responde a las peticiones que le enviemos.

Para comenzar necesitamos utilizar un servidor web, puede ser un servidor de los que están operativos en Internet, o bien, utilizar nuestro propio servidor. En los ejemplos voy a utilizar un servidor Apache 2 que he instalado en un contenedor LXD, cuya dirección IP es 192.168.1.1, y contiene dos páginas web. La primera index.html, contiene el típico “Hola Mundo!” Y la segunda página, llamada ejemplo.html, contiene una imagen que se encuentra albergada en el fichero foto.jpg.

El primer paso consiste en conectarnos al servidor, en este caso utilizamos el comando telnet de la siguiente forma:

$ telnet 192.168.1.1 80

El segundo parámetro, 80, le indica a telnet que abra una conexión al servidor indicado por la dirección IP al puerto 80 TCP. Hay que tener en cuenta que si no se indica el puerto, telnet intentará conectarse al puerto 23 TCP, que es el que tiene asignado por defecto el protocolo TELNET.

Una vez abierta la conexión, todo lo que escribamos se le envía tal cual al servidor una vez que presionemos la tecla ENTER. Por otro lado, todo lo que envíe el servidor será mostrado por pantalla.

Ahora vamos a proceder a enviar al servidor el comando GET para que nos proporcione la página index.html. Los servidores web, normalmente, devuelven la página index.html en el caso de que el cliente no le solicite un recurso en concreto. A esta página se le denomina página de índice y el nombre del fichero correspondiente se puede configurar en el servidor. En este caso no vamos a indicar el recurso, simplemente le solicitamos que nos envíe la página de índice con /. Por último, se indica la versión del protocolo que queramos utilizar. En este caso, al ser la versión HTTP/1.0, el servidor nos enviará la respuesta (cabecera y cuerpo) y automáticamente cerrará la conexión.

$ telnet 192.168.1.1 80
Trying 192.168.1.1...
Connected to 192.168.1.1.
Escape character is '^]'.
GET / HTTP/1.0

HTTP/1.1 200 OK
Date: Fri, 21 Dec 2018 12:34:01 GMT
Server: Apache/2.4.29 (Ubuntu)
Last-Modified: Fri, 21 Dec 2018 12:33:43 GMT
ETag: "2c-57d87752b253a"
Accept-Ranges: bytes
Content-Length: 44
Connection: close
Content-Type: text/html

<html>
<body>
Hola Mundo!
</body>
</html>
Connection closed by foreign host.

Se ha resaltado en rojo lo que el lector tiene que escribir en la terminal. El resto del texto es la respuesta del servidor.

Si quisiéramos sólo la cabecera y no el cuerpo, podemos utilizar del mismo modo, el comando HEAD. Este comando es muy útil a la hora de comprobar el funcionamiento del servidor. En estos casos, solemos estar interesado en ver los campos de la cabecera de la respuesta y no los datos que vienen en el cuerpo.  

telnet 192.168.1.1 80
Trying 192.168.1.1...
Connected to 192.168.1.1.
Escape character is '^]'.
HEAD / HTTP/1.0

HTTP/1.1 200 OK
Date: Fri, 21 Dec 2018 12:39:33 GMT
Server: Apache/2.4.29 (Ubuntu)
Last-Modified: Fri, 21 Dec 2018 12:33:43 GMT
ETag: "2c-57d87752b253a"
Accept-Ranges: bytes
Content-Length: 44
Connection: close
Content-Type: text/html
Connection closed by foreign host.

Para solicitar un recurso que no sea el índice, tendremos que indicarlo explícitamente en el segundo parámetro. Si fuera necesario, habría que incluir la ruta completa al recurso en cuestión.

telnet 192.168.1.1 80
Trying 192.168.1.1...
Connected to 192.168.1.1.
Escape character is '^]'.
GET /ejemplo.html HTTP/1.0

HTTP/1.1 200 OK
Date: Fri, 21 Dec 2018 17:33:01 GMT
Server: Apache/2.4.29 (Ubuntu)
Last-Modified: Fri, 21 Dec 2018 17:24:54 GMT
ETag: "37-57d8b8684f3d9"
Accept-Ranges: bytes
Content-Length: 55
Connection: close
Content-Type: text/html

<html>
<body>
<img src="foto.jpg" />
</body>
</html>
Connection closed by foreign host.

Ahora vemos como el servidor nos responde con la página correspondiente y nos cierra la conexión. Por lo tanto, para obtener la imagen tendremos que volver a conectarnos y solicitar sólo este recurso.

telnet 192.168.1.1 80
Trying 192.168.1.1...
Connected to 192.168.1.1.
Escape character is '^]'.
GET /foto.jpg HTTP/1.0

HTTP/1.1 200 OK
Date: Fri, 21 Dec 2018 17:35:43 GMT
Server: Apache/2.4.29 (Ubuntu)
Last-Modified: Fri, 21 Dec 2018 17:30:56 GMT
ETag: "1447b-57d8b9c17834a"
Accept-Ranges: bytes
Content-Length: 83067
Connection: close
Content-Type: image/jpeg
�[o�' �!pc2� �GG3f*�4Ɲ�ѺO�d�#�?��L& %C��� 8�1�g
>��K�;�����3'D~� ߤcכ�G'b�!\� �a��0� ��(:�kx( " ++�u �� y���8� @��!m0�!�� OL                                �s�!@�)2� ���T7q�� {`xjfO� ��!"��J���
                                                                    0�h�Lj �.*_0� �� �r��0!)?� �h�� N�s�(�� C �?~��U � �����`�I�� &8L'1�
?�����8�?�1� �8

�,�$?A��(0�B9 �8f �D���l|Br��"�� YA|��_n%��� I� �En0pJ �� \ !r��փ�/�G0�I?����8BfG �� L��!�Q��K�\<���V#'����M-L��_HX+��/��8��� p���/��j
G ����                                                               p
      b��� g� � ��0��$�

�:���%�|a GQ 8 p��y��xB��j`�y�����<���@��8���� �\
...
Connection closed by foreign host.

Como se puede apreciar, la imagen viene en formato binario por ello vemos todos esos caracteres extraños por pantalla (sólo muestro los primeros bytes). Los navegadores lo guardan en un buffer y a continuación descomprimen la imagen y la muestran en la ventana. Algo que no podemos hacer nosotros en este ejemplo.

Respuestas del Servidor

Si nos fijamos en las respuestas del servidor, vemos que siempre nos responde con una primera línea en la cabecera indicando la versión del protocolo seguido del código de estado y la descripción textual del mismo, tal y como hemos comentado anteriormente. A parte, en la cabecera aparecen otros campos, en los que se nos indica la fecha, el software del servidor HTTP, así como el sistema operativo utilizado, el tamaño en bytes del cuerpo, ... 

A la hora de simular nosotros el comportamiento del servidor (véase más adelante), no estamos obligados a incluir todos estos campos, ya que no son obligatorios.

VirtualHosts y HTTP/1.1

Es normal que los servidores web alberguen múltiples sitios al mismo tiempo. La técnica más habitual para implementar esta solución consiste en que el servidor web seleccione el sitio correspondiente basándose en el nombre de dominio. Esto implica que los diferentes sitios comparten la misma dirección IP y puerto, por ello, no podremos acceder sólo con su dirección IP sin especificar el dominio. Para que el servidor pueda determinar el nombre de dominio, el cliente lo tiene que incluir en la cabecera en un campo denominado Host. Por otro lado, si utilizamos la versión HTTP/1.1 del protocolo debemos incluir también este campo ya que si no el servidor nos devolverá un error indicándonos que la consulta es errónea. En este caso usamos www.ejemplo.lan:

telnet 192.168.1.1 80
Trying 192.168.1.1...
Connected to 192.168.1.1.
Escape character is '^]'.
GET / HTTP/1.0
Host: www.ejemplo.lan

HTTP/1.1 200 OK
Date: Fri, 21 Dec 2018 17:51:48 GMT
Server: Apache/2.4.29 (Ubuntu)
Last-Modified: Fri, 21 Dec 2018 17:23:57 GMT
ETag: "2c-57d8b831e4ae1"
Accept-Ranges: bytes
Content-Length: 44
Content-Type: text/html

<html>
<body>
Hola Mundo!
</body>
</html>
Connection closed by foreign host.

Pero esta vez podremos comprobar que la conexión no se cierra de forma instantánea tras recibir los datos, sino tras varios segundos. Esto se debe a que el servidor mantiene la conexión abierta para seguir respondiendo a futuras peticiones, pero si no recibe ninguna tras un tiempo predefinido (timeout) cerrará la conexión.

Simulando un servidor HTTP

Ahora vamos a simular el comportamiento de un servidor web y para ello vamos a utilizar la herramienta de la línea de comandos netcat o nc. Esta herramienta nos permite crear conexiones TCP con otros servicios. El parámetro -l <puerto> indica que abra una conexión y se quede a la escucha en el puerto TCP que indiquemos. Ahora, con un navegador web podremos conectarnos a este puerto y ver que datos envía normalmente el navegador. Si queremos simular el comportamiento del servidor, tendremos que responder al navegador con una cabecera y un cuerpo. Vamos a ver un ejemplo,

$ netcat -C -l 9999
GET / HTTP/1.1
Host: localhost:9999
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: es-ES,es;q=0.9,it;q=0.8,en;q=0.7,fr-FR;q=0.6,fr;q=0.5

HTTP/1.1 200 OK

<html>
<body>
Hola navegador!
</body>
</html>
^C

El parámetro -C del comando netcat, le indica que interprete los saltos de línea como dos caracteres CR+LF. Tras ejecutar el comando, se queda esperando a recibir conexiones en el puerto indicado. Lo que hemos hecho es abrir un navegador web, Chrome en este ejemplo, y acceder a la URL http://localhost:9999. A partir de este momento es el navegador el que se queda esperando la respuesta del servidor y en la terminal nos aparece la petición HTTP junto con todos los campos que acompañan a la petición efectuada por el navegador.

Para que el navegador reciba la respuesta, tecleamos a continuación en la terminal las líneas que aparecen en rojo. Por último pulsamos Ctrl-C para cerrar la conexión e informar al navegador que hemos finalizado el envío.

Probando los códigos de estado 3xx y 4xx

Los códigos 3xx le indican al cliente que el sitio web se encuentra en otra ubicación. Podemos utilizarlos para reenviar al cliente a otra dirección. Vamos a probarlo en el siguiente ejemplo, donde con este código vamos a redirigir al navegador a Google.

$ netcat -C -l 9999
GET / HTTP/1.1
Host: localhost:9999
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: es-ES,es;q=0.9,it;q=0.8,en;q=0.7,fr-FR;q=0.6,fr;q=0.5

HTTP/1.1 301 Moved Permanently
Location: http://www.google.com

En este caso hemos utilizado el código 301 que indica que el sitio web se ha trasladado de forma permanente a la dirección que indique el campo Location. Al finalizar, pulsamos dos veces la tecla ENTER y veremos como el navegador se va automáticamente a Google.

Si queremos producir un error, podemos hacerlo con telnet y ver como responde el servidor. En este caso, vamos a solicitar un recurso que no exista en el servidor y veremos como se genera el típico error 404 Not Found:

telnet 192.168.1.1 80
Trying 192.168.1.1...
Connected to 192.168.1.1.
Escape character is '^]'.
GET /texto.html HTTP/1.0

HTTP/1.1 404 Not Found
Date: Fri, 21 Dec 2018 18:23:14 GMT
Server: Apache/2.4.29 (Ubuntu)
Content-Length: 285
Connection: close
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL /prueba.html was not found on this server.</p>
<hr>
<address>Apache/2.4.29 (Ubuntu) Server at apache.lxd Port 80</address>
</body></html>
Connection closed by foreign host.

También podemos simular este comportamiento desde el punto de vista del servidor y ver como el navegador muestra la página de error que le enviemos.

Y con esto finalizamos. Recomiendo al lector que no sólo realice los ejemplos que aparecen ilustrados en este artículo, sino que pruebe otros y vea como se comportan diferentes navegadores y servidores web.



Comentarios

  1. Muchas gracias por la publicación del artículo, como siempre una sobresaliente explicación.
    Galvez.

    ResponderEliminar

Publicar un comentario

Entradas populares de este blog

Instalando Moodle con Docker

En este blog ya hemos hablado en varios artículos sobre la tecnología de contenedores, pero hasta ahora nos habíamos centrado en LXD . En este artículo vamos a explicar cómo podemos instalar Moodle en menos de un minuto (dependiendo de la velocidad de descarga que se tenga, se puede alargar un poco más) usando contenedores. Acerca de Moodle No voy a explicar que es Moodle ni como instalarlo desde cero, para eso existe en Internet multitud de tutoriales. Lo que sí quiero comentar es que para instalar Moodle hace falta un servidor web con PHP . Además requiere que PHP tenga instalado una serie de componentes adicionales. Por otro lado, necesitamos tener instalado en el servidor un sistema de gestión de bases de datos relacional, ya que Moodle almacena la información en él. Normalmente se utiliza MySQL , MariaDB o PostgreSQL . También debemos crear una base de datos específica para Moodle con su respectivo usuario. Durante la instalación Moodle creará las tablas necesari

ZFS, Primera parte

Cuando el año pasado instalé LXD y lo configuré por primera vez, me encontré que podía utilizar, de hecho se recomienda, el sistema de ficheros ZFS para albergar los contenedores. Posteriormente, cuando instalé Proxmox en el servidor de mi departamento, me encontré de nuevo con  ZFS . Anteriormente no le había prestado mucha atención a  ZF S , normalmente utilizo EXT4 o XFS , pero estaba claro que había una estrecha relación entre  ZFS  y los sistemas de virtualización. ZFS  es un sistema de ficheros desarrollado por Sun Microsystems  (creadores también del lenguaje de programación Java ), posteriormente la empresa fue adquirida por Oracle , actuales propietarios. OpenZFS  es la variante libre y posee una licencia de tipo  CDDL , que aunque es software libre, es incompatible con GPL . Por este motivo, el kernel de Linux no lo incorpora de serie. Sin embargo, los usuarios pueden instalarlo sin problemas ya que se encuentra en los repositorios de la mayoría de las distribucione