Die Anatomie von cURL: Wie Sie das Tool nutzen können, um die Antwort eines Origin-Servers zu testen
Kennen Sie das Gefühl, dass es manchmal nur um ein einziges Thema geht? Als Sales Engineer unterstütze ich unsere Kunden bei der Erstellung und Bereitstellung von Systemen, und vergangene Woche ging es dabei definitiv hauptsächlich um ein Thema.
Ich wurde mit etwas konfrontiert, das ich bisher für einen Nischenanwendungsfall gehalten hatte: Ich musste ein bestimmtes Tool (cURL) auf eine bestimmte Art und Weise nutzen, um die Reaktion eines Origin-Servers zu testen (z. B. ob eine Anwendung als Antwort eine ungewöhnliche Fehlermeldung zurückgibt). Und dieses Thema verfolgte mich immer weiter! Ich erklärte den Prozess einigen Kollegen, die damit noch nicht so vertraut waren, sowie einigen Kunden, die Fehler bei ihren Bereitstellungen beheben wollten. Und dabei entstand die Idee für diesen Blogpost. Legen wir doch am besten gleich los.
### cURL: Die Hintergründe
Client URL (auch als cURL oder curl bekannt) wurde [im Jahr 1997 von Daniel Stenberg](https://en.wikipedia.org/wiki/CURL) veröffentlicht, der das Projekt seitdem fleißig betreut. Ursprünglich wurde dieses Tool entwickelt, um den Abruf von Wechselkursen für IRC Nutzer zu automatisieren. Inzwischen wird es aber für sämtliche URL-Abrufe verwendet. Seit Stenberg das Projekt ins Leben rief, hat er es ständig weiterentwickelt und neue Funktionen hinzugefügt, sodass es inzwischen zu einem Grundpfeiler für andere Projekte geworden ist.
Bei Curl handelt es sich um ein Kommandozeilen-Tool, mit dem sich HTTP-Requests an eine URL senden und die Ergebnisse empfangen lassen. Bei Betriebssystemen wie macOS und vielen Linux Distributionen ist es bereits standardmäßig enthalten. Und da ein Großteil des Internets auf HTTP ausgerichtet ist, ist es ein großartiges Tool, um Webseiten, APIs oder alles andere mit einer HTTP-Schnittstelle anzusteuern.
Zu Demozwecken verwenden wir curl, um das Browserverhalten beim Anfordern einer Webseite zu simulieren. So behalten wir die vollständige Kontrolle über die Anfrage, was die Fehlersuche erheblich erleichtert.
Nachfolgend finden Sie einen einfachen curl Befehl, den ich mit dem Terminalprogramm auf meinem MacBook ausgeführt habe. Mit diesem Befehl wird die Fastly Website aufgerufen und der vollständige HTML-Code angezeigt. Der Output ist zwar etwas unübersichtlich, aber um die Bereinigung kümmern wir uns später.
curl https://www.fastly.com/
### Flags
Die eigentliche Stärke von curl besteht darin, dass man Requests mit Flags manipulieren kann. Indem man zum Beispiel `-I` hinzufügt, werden als Antwort nur die Header vom Remote-Server ausgegeben, und nicht die Inhalte.
Ich verwende bei fast allen curls als Flags `-svo`, wobei „s“ für „silent mode“, „v“ für „verbose“ und „o“ für „writing to a file“ steht. Diese setze ich auf `/dev/null`. Der vollständige curl Befehl lautet also `curl -svo /dev/null https://www.fastly.com`. So kann ich mich auf die genauen Requests konzentrieren, ohne von einer unübersichtlichen Antwort abgelenkt zu werden. Die Header sind mit praktischen kleinen Flags versehen, die anzeigen, ob sie gesendet oder empfangen wurden, und der SSL-Handshake wird vor der Hauptanfrage angezeigt.
Nachfolgend sehen Sie den schriftlichen Befehl ($), die Verbindung und den SSL-Handshake (*), die Anfrage (>) und die Antwort (<).
```shell
$ 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
```
### URL- und SSL-Name
Das obige Szenario zeigt eine Situation, in der der SSL-Zertifikatname, der Host Header und der DNS-Name alle den Wert `www.fastly.com` haben. Manchmal braucht es eben nicht mehr! Ab hier wollen wir diese Elemente allerdings getrennt betrachten, um verschiedene Aspekte für unterschiedliche Fehlersuchziele untersuchen zu können.
So, wie wir `https://www.fastly.com/` aktuell nutzen, wird die angefragte URL angezeigt. Der in der URL verwendete Hostname (z. B. `www.fastly.com`) wird von curl als Wert genutzt, um den Namen des SSL-Zertifikats anzufragen und zu verifizieren. Dies ist wichtig, um zu überprüfen, ob Ihr TLS wie gewünscht funktioniert und ob Ihre Website mit dem von Ihnen erwarteten Zertifikat sicher ist. Hier ein Beispiel:
$ 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. --->{Aus Gründen der Lesbarkeit gekürzt}<---
### Pfad
Die beiden anderen Dinge, die bei der Original-URL verbleiben, sind das Schema (z. B. HTTP oder HTTPS) und der Pfad zu der angefragten Ressource. Ich betrachte die URL als das Ziel, und alles andere ist nur die Art und Weise, wie oder wo wir sie anfragen.
### Host Header
Ein typischer Webserver kann mehrere Sites mit unterschiedlichen Domainnamen wie `blog.example.com` oder `docs.example.com` hosten. Auch wenn sich diese Websites auf demselben System befinden, können der Quellcode bzw. der URL-Pfad unterschiedlich sein.
Wenn wir den Host Header ändern wollen, können wir diese Änderung in curl durch die ausdrückliche Definition des Headers übergeben. Header können entweder mit `--header`- oder mit den kürzeren `-H`-Flags gekennzeichnet sein. Der Wert steht dabei zwischen Anführungszeichen und der Name des Headers wird definiert. Der curl Befehl sieht folgendermaßen aus:
```shell
curl -svo /dev/null https://www.fastly.com/ -H “host: blog.fastly.com”
```
Mit diesem Request können wir den Host von `blog.fastly.com` abfragen, indem wir das Zertifikat und die Location von `www.fastly.com` verwenden. Dies ist besonders dann nützlich, wenn wir die Unterschiede in der Apex zweier Domains überprüfen möchten, z. B. `fastly.com` vs. `www.fastly.com`. Im Folgenden können wir sehen, dass ein korrekter 301-Redirect zu `www`. stattfindet.
$ 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) --->{Aus Gründen der Lesbarkeit gekürzt}<--- * 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
Von allen Flags in curl ist `--resolve` wahrscheinlich das am meisten unterschätzte. Ganz am Anfang jeder oben gezeigten curl sehen Sie `Trying X.X.X.X` als IP-Adresse, die den Domainnamen aus dem DNS auflöst. Manchmal ist der DNS-Name aber nicht das eigentliche Ziel, das Sie ansteuern möchten. Möglicherweise handelt es sich dabei um eine anfängliche Bereitstellung, und wir testen, ob der Service korrekt aufgelöst wird, bevor wir den DNS ändern, um diese Änderung zu veröffentlichen. Vielleicht gibt es aber auch eine Kette von Reverse Proxies, bei der der öffentliche DNS nur den Anfang der Kette auflöst, wir aber wissen wollen, wie der Origin-Server selbst reagiert.
Um diese Probleme zu umgehen, können wir den Domainnamen für curl selbst auflösen und jede beliebige IP-Adresse angeben. Hierfür sind folgende beiden Schritte notwendig:
1. Sofern Sie die IP-Adresse nicht bereits kennen, müssen Sie möglicherweise eine DNS-Auflösung des Hosts durchführen, um diese zu erhalten. Ich verwende am liebsten Dig, ein weiteres Standardprogramm, das diese DNS-Abfrage über die Command Line durchführt.
```shell
$ dig www.fastly.com +short
prod.www-fastly-com.map.fastly.net.
151.101.185.57
```
2. Als Nächstes können wir die IP-Adresse zuweisen, die von curl beim HTTP Request verwendet werden soll, indem wir die Option --resolve zusammen mit dem zu ersetzenden Domainnamen, dem Port und der zu verwendenden IP einsetzen. Das Ganze sieht dann wie folgt aus:
$ 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) --->{Aus Gründen der Lesbarkeit gekürzt}<---
### Alternativlösung: Connect-to
Als Alternative zur obigen Funktion „Resolve“ können wir auch `--connect-to` verwenden und entweder eine IP-Adresse oder einen Host übermitteln, mit denen eine Verbindung hergestellt werden soll. Dies ist zwar einfacher, aber bei der Verwendung eines Hosts ist die Genauigkeit nicht ganz so hoch. „Connect-to“ führt eine DNS-Auflösung für Sie durch. Sie müssen sich dann allerdings darauf verlassen, dass die DNS-Auflösung die richtige IP-Adresse nutzt. Dies ist kein Problem, wenn Sie definitiv wissen, dass es eine 1:1-Zuordnung von Host zu IP gibt. Manchmal haben Sie aber mehrere A-Einträge und möchten alle IP-Adressen ausprobieren, oder der Host verfügt über ein DNS-basiertes Loadbalancing, sodass verschiedene Locations oder Versuche zu unterschiedlichen Ergebnissen führen können. Indem Sie die Funktionen `--resolve` oder `--connect-to` mit einer IP-Adresse nutzen, können Sie genauere Ergebnisse erzielen. Wenn Sie diese curl also zum Beispiel mit Ihren Kolleg*innen teilen, sind Ihre jeweiligen Ergebnisse mit höherer Wahrscheinlichkeit vergleichbar.
Ein weiterer Vorteil der Verwendung von „connect-to“ ist, dass Sie die explizite Angabe des Domainnamens, für den Sie einen DNS wie bei „resolve“ definieren möchten, sowie des Ports überspringen können. Sie sehen also zum Beispiel `::151.101.185.57` oder können `::target.host.fastly.com` verwenden.
```shell
$ 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)
--->{Aus Gründen der Lesbarkeit gekürzt}<---
```
### Zusammenfassung
Es ist erwähnenswert, dass es viele Techniken zur Durchführung von HTTP Requests gibt. Curl bietet verschiedene Methoden, die alle ihre Vor- und Nachteile haben, und jeder Entwickler hat scheinbar seinen eigenen Favoriten. Meiner ist der, den ich Ihnen soeben gezeigt habe.
Zusammenfassend lässt sich sagen, dass wir durch das Schreiben von „/dev/null“, die Manipulation des „host“-Headers und die Verwendung der „resolve“-Funktionen genau bestimmen können, wohin wir eine Anfrage im Internet senden möchten, welches SSL-Zertifikat wir verwenden möchten und nach welchem Host der Server mit einem vorhersehbaren und einfach zu verwaltenden Output Ausschau halten soll. Dies erleichtert die Fehlersuche und erspart Ihnen hoffentlich unzählige Stunden Frust. Probieren Sie es doch einfach mal selbst!
Das folgende Snippet zeigt den vollständigen Code, der zur leichteren Orientierung farbig markiert ist:
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