Anatomía de curl: cómo usar cURL para probar la respuesta de un servidor de origen
¿Alguna vez has tenido una de esas semanas en las que todo parece seguir el mismo patrón? Como ingeniero de ventas, trabajo con nuestros clientes para crear y desplegar sistemas, y la semana pasada tuvo un claro patrón.
Me llegó lo que parecía en principio un caso de uso único, en el que usé una herramienta concreta (cURL) de una manera particular para probar cómo respondía un servidor de origen. Este procedimiento te puede servir, por ejemplo, si quieres hacer pruebas cuando una aplicación responde con mensajes de error raros. Bueno, ¡pues no paraban de llegar casos de ese tipo! Expliqué el funcionamiento a varios compañeros y a unos cuantos clientes que intentaban resolver problemas con sus despliegues, y pensé que estaría bien hacer una entrada en el blog para todo el mundo. Empecemos.
El trasfondo de cURL
Client URL (también llamado cURL o curl ) fue lanzado en 1997 por Daniel Stenberg, que se ha mantenido al frente del proyecto desde entonces. En su momento se creó para automatizar la obtención del tipo de cambio de divisas para usuarios de IRC, pero en la actualidad se usa para todo tipo de recuperaciones de URL. Desde su lanzamiento, Daniel ha seguido desarrollando y añadiendo funcionalidades al proyecto a medida que se convertía en una pieza básica de otros proyectos. Es todo un héroe.
Curl es una utilidad de línea de comandos que permite enviar una petición HTTP a una URL y recibir el resultado. Viene por defecto en sistemas operativos como macOS y muchas distribuciones de Linux. Como la gran mayoría de Internet gira en torno a HTTP, es una herramienta ideal para rebuscar en páginas web, API o cualquier otro elemento con una interfaz HTTP.
En nuestra demostración, utilizaremos curl para simular la experiencia de solicitar una página web desde el navegador. Así podemos tener un control total de la petición y resolver problemas con mucha más facilidad.
A continuación tenemos un comando curl sencillo que ejecuté desde la aplicación Terminal de mi MacBook. Este comando solicita la página de inicio de fastly.com y muestra el HTML completo. Atención, porque este resultado incluye mucho ruido que ya limpiaremos luego.
curl https://www.fastly.com/
Marcas
Curl abre muchas posibilidades al poder manipularse la petición mediante marcas. Por ejemplo, enviar -I
limita la respuesta a solamente los encabezados del servidor remoto, sin el contenido.
Las marcas que uso en casi todos los curl son -svo
, que significan modo silencioso (s), modo detallado (v) y escribiendo en un archivo (o), que escribo en /dev/null
. El comando curl completo queda como curl -svo /dev/null https://www.fastly.com
. Así, puedo centrarme en la petición detallada sin las distracciones de un cuerpo de respuesta lleno de ruido. Los encabezados tienen una pequeña marca que indica si se han enviado o recibido, y la negociación SSL se muestra antes de la petición principal.
A continuación puedes ver el comando tal como va escrito ($), la negociación de conexión y SSL (*), la petición (>) y la respuesta (<).
$ curl -svo /dev/null https://www.fastly.com/
* Trying 199.232.77.57...
* TCP_NODELAY set
* Connected to www.fastly.com (199.232.77.57) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
} [228 bytes data]
* TLSv1.2 (IN), TLS handshake, Server hello (2):
{ [102 bytes data]
* TLSv1.2 (IN), TLS handshake, Certificate (11):
{ [2828 bytes data]
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
{ [300 bytes data]
* TLSv1.2 (IN), TLS handshake, Server finished (14):
{ [4 bytes data]
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
} [37 bytes data]
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.2 (OUT), TLS handshake, Finished (20):
} [16 bytes data]
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
{ [1 bytes data]
* TLSv1.2 (IN), TLS handshake, Finished (20):
{ [16 bytes data]
* SSL connection using TLSv1.2 / ECDHE-RSA-CHACHA20-POLY1305
* ALPN, server accepted to use h2
* Server certificate:
* subject: C=US; ST=California; L=San Francisco; O=Fastly, Inc.; CN=www.fastly.com
* start date: Mar 3 21:56:03 2021 GMT
* expire date: Apr 4 21:56:03 2022 GMT
* subjectAltName: host "www.fastly.com" matched cert's "www.fastly.com"
* issuer: C=BE; O=GlobalSign nv-sa; CN=GlobalSign RSA OV SSL CA 2018
* SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fe1f980aa00)
> GET / HTTP/2
> Host: www.fastly.com
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 100)!
< HTTP/2 200
< alt-svc: h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400
< etag: "5c770df920f8c90e4c4532c32aea6ec3"
< content-type: text/html
< accept-ranges: bytes
< date: Mon, 09 Aug 2021 15:38:35 GMT
< x-served-by: cache-pwk4963-PWK
< x-cache: HIT
< x-cache-hits: 2
< x-timer: S1628523515.208976,VS0,VE0
< vary: Accept-Encoding
< x-xss-protection: 1; mode=block
< x-frame-options: DENY
< x-content-type-options: nosniff
< cache-control: max-age=0, private, must-revalidate
< server: Artisanal bits
< strict-transport-security: max-age=31536000
< content-length: 777219
<
{ [1113 bytes data]
* Connection #0 to host www.fastly.com left intact
* Closing connection 0
Nombre de SSL y URL
En el ejemplo anterior, el nombre del certificado SSL, el encabezado del host y el nombre del DNS tienen el mismo valor: www.fastly.com
. A veces, eso es todo lo que necesitas. A partir de aquí, sin embargo, separaremos estos elementos para repasar aspectos de distintos objetivos de resolución de problemas.
El uso actual de https://www.fastly.com/
muestra la URL que se solicita. El nombre de host utilizado dentro de la URL, sea cual sea (por ejemplo, www.fastly.com
), será el valor que curl utiliza para solicitar y verificar el nombre del certificado SSL. Esto es importante para validar que TLS funcione como esperas, y que tu sitio esté protegido por el certificado previsto. Veamos el ejemplo siguiente:
$ curl -svo /dev/null https://www.fastly.com/ * Trying 199.232.77.57... * TCP_NODELAY set * Connected to www.fastly.com (199.232.77.57) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/cert.pem CApath: none * TLSv1.2 (OUT), TLS handshake, Client hello (1): } [228 bytes data] * TLSv1.2 (IN), TLS handshake, Server hello (2): { [102 bytes data] * TLSv1.2 (IN), TLS handshake, Certificate (11): { [2828 bytes data] * TLSv1.2 (IN), TLS handshake, Server key exchange (12): { [300 bytes data] * TLSv1.2 (IN), TLS handshake, Server finished (14): { [4 bytes data] * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): } [37 bytes data] * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): } [1 bytes data] * TLSv1.2 (OUT), TLS handshake, Finished (20): } [16 bytes data] * TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): { [1 bytes data] * TLSv1.2 (IN), TLS handshake, Finished (20): { [16 bytes data] * SSL connection using TLSv1.2 / ECDHE-RSA-CHACHA20-POLY1305 * ALPN, server accepted to use h2 * Server certificate:
* subject: C=US; ST=California; L=San Francisco; O=Fastly, Inc.; CN=www.fastly.com
* start date: Mar 3 21:56:03 2021 GMT * expire date: Apr 4 21:56:03 2022 GMT * subjectAltName: host "www.fastly.com" matched cert's "www.fastly.com" * issuer: C=BE; O=GlobalSign nv-sa; CN=GlobalSign RSA OV SSL CA 2018 * SSL certificate verify ok. --->{Truncated for readability}<---
Ruta
Los otros dos elementos que permanecen de la URL original son el esquema, como HTTP o HTTPS, y la ruta hasta el recurso concreto que se solicita. Me gusta pensar en la URL como el objetivo final, mientras que todo lo demás es simplemente cómo o dónde la estamos pidiendo.
Encabezado del host
El típico servidor web puede alojar múltiples sitios con distintos nombres de dominios, como blog.ejemplo.com
o docs.ejemplo.com
. Aunque los sitios puedan encontrarse en el mismo sistema, el código fuente o la ruta de la URL podrían ser diferentes.
Si queremos cambiar el encabezado del host, podemos enviarlo definiendo explícitamente el encabezado dentro de curl . Los encabezados pueden declararse con la marca --header
o, abreviada, con -H
. Luego, el valor se encapsula con comillas y se define el nombre del encabezado. El comando curl queda así:
curl -svo /dev/null https://www.fastly.com/ -H “host: blog.fastly.com”
Esta petición pregunta por el host de blog.fastly.com
utilizando el certificado y la ubicación de www.fastly.com
. Esto resulta muy útil si quieres comprobar la diferencia entre la raíz de un dominio, como entre fastly.com
y www.fastly.com
. Aquí vemos que proporciona correctamente un 301 a www
.
$ curl -svo /dev/null https://www.fastly.com/ -H "host: fastly.com" * Trying 199.232.77.57... * TCP_NODELAY set * Connected to www.fastly.com (199.232.77.57) port 443 (#0) --->{Truncated for readability}<--- * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 * Using Stream ID: 1 (easy handle 0x7fa15480aa00) > GET / HTTP/2
> Host: fastly.com
> User-Agent: curl/7.64.1 > Accept: */* > * Connection state changed (MAX_CONCURRENT_STREAMS == 100)! < HTTP/2 301 < retry-after: 0 < accept-ranges: bytes < date: Mon, 09 Aug 2021 16:26:07 GMT < x-served-by: cache-pwk4938-PWK < x-cache: HIT < x-cache-hits: 0 < cache-control: max-age=0, private, must-revalidate < server: Artisanal bits < strict-transport-security: max-age=31536000 < location: https://www.fastly.com/ < content-length: 0 < { [0 bytes data] * Connection #0 to host www.fastly.com left intact * Closing connection 0
Resolve
De todas las marcas dentro de curl , --resolve
es seguramente la más subestimada. Fíjate que al principio del todo de cada curl que hemos mostrado aparece Trying X.X.X.X
como dirección IP, resolviendo el nombre del dominio desde el DNS. Sin embargo, hay ocasiones en las que el nombre del DNS no es el objetivo real al que te gustaría llegar. Puede que busques algún despliegue inicial y estés probando que el servicio se resuelva bien antes de cambiar el DNS para difundir dicho cambio, o tal vez hay una cadena de proxis inversos, donde el DNS público solo se resuelve al principio de la cadena, pero queremos saber cómo responde el origen en sí.
Para eludir estas dificultades, podemos resolver nosotros mismos el nombre de dominio de curl y proporcionar la dirección IP que queramos. Para ello, tenemos que seguir un proceso de dos pasos:
- A menos que ya conozcas la IP a la que quieres dirigirte, puede que sea necesario realizar una resolución de DNS del host para captar la IP. Me gusta usar Dig, otra utilidad estándar que realiza esa consulta de DNS desde la línea de comandos.
$ dig www.fastly.com +short
prod.www-fastly-com.map.fastly.net.
151.101.185.57
- A continuación, podemos asignar esta IP para que la use curl al realizar la petición HTTP utilizando la opción --resolve, junto con el nombre de dominio que se reemplazará, el puerto y la IP que se utilizará. Todo junto queda así:
$ curl -svo /dev/null https://www.fastly.com/ -H "host: fastly.com" --resolve www.fastly.com:443:151.101.185.57
* Added www.fastly.com:443:151.101.185.57 to DNS cache * Hostname www.fastly.com was found in DNS cache * Trying 151.101.185.57...
* TCP_NODELAY set * Connected to www.fastly.com (151.101.185.57) port 443 (#0) --->{Truncated for readability}<---
La alternativa connect-to
Como alternativa a la función resolve que acabamos de explicar, también puedes usar --connect-to
y pasar una IP o un host para conectarte. Esta opción es más fácil, pero hay que decir que la precisión no es la misma si se utiliza un host. Connect-to realizará una resolución de DNS por ti, pero entonces quedas en manos de que la resolución de DNS obtenga la IP correcta. Esto no plantea ningún problema si sabes con total certeza que hay una correspondencia exacta entre el host y la IP. Sin embargo, hay ocasiones en las que tienes varios registros A y quieres probar todas las IP, u otras veces en las que el host tiene equilibrio de carga basado en el DNS, de modo que intentos o ubicaciones distintos pueden arrojar resultados diferentes. Si usas la función --resolve
o --connect-to
con una IP, puedes dar información explícita. Así, si compartes este curl con alguien, será más probable repetir tus resultados.
Usar connect-to ofrece una pequeña ventaja, y es que no tienes que definir explícitamente el nombre de dominio por el que quieres definir DNS como con resolve, ni tampoco el puerto. Por tanto, verás ::151.101.185.57
o podrías usar ::target.host.fastly.com
.
$ curl -svo /dev/null https://www.fastly.com/ -H "host: fastly.com" --connect-to ::151.101.185.57
* Connecting to hostname: 151.101.185.57
* Trying 151.101.185.57...
* TCP_NODELAY set
* Connected to 151.101.185.57 (151.101.185.57) port 443 (#0)
--->{Truncated for readability}<---
En resumen
Cabe destacar que existen muchas técnicas para realizar una petición HTTP. Curl ofrece métodos alternativos, cada uno con sus ventajas e inconvenientes, de modo que cada experto acaba teniendo su favorito. Este es el mío.
En resumidas cuentas, escribiendo en /dev/null, manipulando el encabezado del host y usando las funcionalidades de resolve, podemos señalar exactamente adónde queremos enviar una petición en Internet, con qué certificado SSL queremos coincidir y a qué host debe estar atento el servidor para obtener una salida predecible y fácil de gestionar. Así es más fácil resolver problemas y, espero, te ahorrarás un sinfín de horas de desesperación. ¡Pruébalo ya!
El siguiente fragmento incluye la forma completa con diferentes colores para que quede claro qué es qué:
curl -svo /dev/null https://www.certificate-name.com/path/to/resource/ -H "host: www.expected-host.com" --resolve www.certificate-name.com:443:1.2.3.4