OpenResty ist das erweiterte NGINX mit dynamischen Anfragen und Antworten

API7.ai

October 23, 2022

OpenResty (NGINX + Lua)

Nach der vorherigen Einführung sollten Sie das Konzept von OpenResty und die Art und Weise, wie man es lernt, verstanden haben. Dieser Artikel wird uns zeigen, wie OpenResty Client-Anfragen und -Antworten verarbeitet.

Obwohl OpenResty ein auf NGINX basierender Webserver ist, unterscheidet es sich grundlegend von NGINX: NGINX wird durch statische Konfigurationsdateien gesteuert, während OpenResty durch die Lua-API gesteuert wird, was mehr Flexibilität und Programmierbarkeit bietet.

Lassen Sie mich Ihnen die Vorteile der Lua-API zeigen.

API-Kategorien

Zunächst müssen wir wissen, dass die OpenResty-API in die folgenden groben Kategorien unterteilt ist.

  • Anfrage- und Antwortverarbeitung.
  • SSL-bezogene.
  • Gemeinsames Wörterbuch.
  • Cosocket.
  • Verarbeitung von Traffic auf der vierten Schicht.
  • Prozess und Worker.
  • Zugriff auf NGINX-Variablen und -Konfiguration.
  • Zeichenketten, Zeit, Kodierung und andere allgemeine Funktionen usw.

Hier schlage ich vor, dass Sie auch die Lua-API-Dokumentation von OpenResty öffnen und sie mit der API-Liste abgleichen, um zu sehen, ob Sie diese Kategorien nachvollziehen können.

OpenResty-APIs existieren nicht nur im lua-nginx-module-Projekt, sondern auch im lua-resty-core-Projekt, wie z.B. ngx.ssl, ngx.base64, ngx.errlog, ngx.process, ngx.re.split, ngx.resp.add_header, ngx.balancer, ngx.semaphore, ngx.ocsp und andere APIs.

Für APIs, die nicht im lua-nginx-module-Projekt enthalten sind, müssen Sie sie separat einbinden, um sie zu verwenden. Wenn Sie beispielsweise die Split-Funktion verwenden möchten, müssen Sie sie wie folgt aufrufen.

$ resty -e 'local ngx_re = require "ngx.re"
 local res, err = ngx_re.split("a,b,c,d", ",", nil, {pos = 5})
 print(res)
 '

Natürlich könnte es Sie verwirren: Im lua-nginx-module-Projekt gibt es mehrere APIs, die mit ngx.re.sub, ngx.re.find usw. beginnen. Warum ist es so, dass die API ngx.re.split die einzige ist, die zuerst eingebunden werden muss, bevor sie verwendet werden kann?

Wie wir im vorherigen Kapitel über lua-resty-core erwähnt haben, werden die neuen OpenResty-APIs im lua-rety-core-Repository durch FFI implementiert, was zwangsläufig zu einer gewissen Fragmentierung führt. Ich hoffe, dass dieses Problem in Zukunft durch die Zusammenführung der Projekte lua-nginx-module und lua-resty-core gelöst wird.

Anfrage

Als nächstes schauen wir uns an, wie OpenResty Client-Anfragen und -Antworten verarbeitet. Zuerst betrachten wir die API zur Verarbeitung von Anfragen, aber es gibt mehr als 20 APIs, die mit ngx.req beginnen. Wie fangen wir also an?

Wir wissen, dass HTTP-Anfragenachrichten aus drei Teilen bestehen: der Anfragezeile, den Anfrageheadern und dem Anfragekörper. Daher werde ich die API in diesen drei Teilen vorstellen.

Anfragezeile

Zuerst die Anfragezeile, die die Anfragemethode, den URI und die HTTP-Protokollversion enthält. In NGINX können Sie diesen Wert mit einer eingebauten Variablen abrufen, während in OpenResty dies der ngx.var.*-API entspricht. Schauen wir uns zwei Beispiele an.

  • Die eingebaute Variable $scheme, die den Namen des Protokolls in NGINX darstellt, ist entweder http oder https; in OpenResty können Sie ngx.var.scheme verwenden, um denselben Wert zurückzugeben.
  • $request_method stellt die Anfragemethode wie GET, POST usw. dar; in OpenResty können Sie denselben Wert über ngx.var.request_method zurückgeben.

Sie können die offizielle NGINX-Dokumentation besuchen, um eine vollständige Liste der NGINX-eingebauten Variablen zu erhalten: http://nginx.org/en/docs/http/ngx_http_core_module.html#variables.

Also stellt sich die Frage: Warum bietet OpenResty eine separate API für die Anfragezeile, wenn Sie die Daten in der Anfragezeile durch die Rückgabe des Werts einer Variablen wie ngx.var.* erhalten können?

Das Ergebnis enthält viele Faktoren:

  • Erstens wird nicht empfohlen, ngx.var wiederholt zu lesen, da dies ineffizient ist.
  • Zweitens, aus Gründen der Programmierfreundlichkeit, gibt ngx.var eine Zeichenkette zurück, kein Lua-Objekt. Es ist schwierig zu handhaben, wenn args mehrere Werte zurückgibt.
  • Drittens, aus Gründen der Flexibilität, sind die meisten ngx.var schreibgeschützt, und nur wenige Variablen sind beschreibbar, wie z.B. $args und limit_rate. Oft müssen wir jedoch die Methode, den URI und die Argumente ändern.

Daher bietet OpenResty mehrere APIs speziell für die Manipulation der Anfragezeile, die die Anfragezeile für nachfolgende Operationen wie Umleitungen neu schreiben können.

Schauen wir uns an, wie die HTTP-Protokollversionsnummer über die API abgerufen wird. Die OpenResty-API ngx.req.http_version macht dasselbe wie die NGINX-Variable $server_protocol: Sie gibt die Versionsnummer des HTTP-Protokolls zurück. Der Rückgabewert dieser API ist jedoch keine Zeichenkette, sondern ein numerisches Format, dessen mögliche Werte 2.0, 1.0, 1.1 und 0.9 sind. Nil wird zurückgegeben, wenn das Ergebnis außerhalb dieses Bereichs liegt.

Schauen wir uns die Methode zur Abfrage der Anfragemethode in der Anfragezeile an. Wie bereits erwähnt, sind die Rolle und der Rückgabewert von ngx.req.get_method und der NGINX-Variablen $request_method gleich: im Zeichenkettenformat.

Allerdings ist das Parameterformat der aktuellen HTTP-Anfragemethode ngx.req.set_method keine Zeichenkette, sondern eingebaute numerische Konstanten. Beispielsweise wird die Anfragemethode durch den folgenden Code in POST umgeschrieben.

ngx.req.set_method(ngx.HTTP_POST)

Um zu überprüfen, ob die eingebaute Konstante ngx.HTTP_POST tatsächlich eine Zahl und keine Zeichenkette ist, können Sie ihren Wert ausdrucken und sehen, ob die Ausgabe 8 ist.

resty -e 'print(ngx.HTTP_POST)'

Auf diese Weise ist der Rückgabewert der get-Methode eine Zeichenkette, während der Eingabewert der set-Methode eine Zahl ist. Es ist in Ordnung, wenn die set-Methode einen verwirrenden Wert übergibt, da die API abstürzen und einen 500-Fehler melden kann. In der folgenden Logik:

if (ngx.req.get_method() == ngx.HTTP_POST) then
    -- etwas tun
 end

Diese Art von Code funktioniert einwandfrei, meldet keine Fehler und ist selbst bei Code-Reviews schwer zu finden. Ich habe einen ähnlichen Fehler gemacht und kann mich noch daran erinnern: Ich hatte bereits zwei Runden von Code-Reviews und unvollständige Testfälle durchlaufen, um ihn abzudecken. Letztendlich hat eine Anomalie in der Produktionsumgebung mich auf das Problem aufmerksam gemacht.

Es gibt keine praktische Möglichkeit, ein solches Problem zu lösen, außer vorsichtiger zu sein oder eine weitere Ebene der Kapselung hinzuzufügen. Wenn Sie Ihre Geschäfts-API entwerfen, können Sie auch überlegen und das konsistente Parameterformat der get- und set-Methoden beibehalten, selbst wenn Sie etwas Leistung opfern müssen.

Darüber hinaus gibt es unter den Methoden zum Umschreiben der Anfragezeile zwei APIs, ngx.req.set_uri und ngx.req.set_uri_args, die verwendet werden können, um URI und Argumente umzuschreiben. Schauen wir uns diese NGINX-Konfiguration an.

rewrite ^ /foo?a=3? break;

Wie können wir dies also mit der entsprechenden Lua-API lösen? Die Antwort sind die folgenden zwei Codezeilen.

ngx.req.set_uri_args("a=3")
ngx.req.set_uri("/foo")

Wenn Sie die offizielle Dokumentation gelesen haben, werden Sie feststellen, dass ngx.req.set_uri einen zweiten Parameter hat: jump, der standardmäßig "false" ist. Wenn Sie ihn auf "true" setzen, entspricht dies dem Setzen des Flags des rewrite-Befehls auf last anstelle von break im obigen Beispiel.

Ich bin jedoch kein großer Fan der Flag-Konfiguration des rewrite-Befehls, da sie unlesbar und unerkennbar ist und weit weniger intuitiv und wartbar als Code.

Anfrageheader

Wie wir wissen, sind HTTP-Anfrageheader im Format key : value, zum Beispiel:

  Accept: text/css,*/*;q=0.1
  Accept-Encoding: gzip, deflate, br

In OpenResty können Sie ngx.req.get_headers verwenden, um die Anfrageheader zu parsen und zu erhalten, und der Rückgabewerttyp ist eine Tabelle.

local h, err = ngx.req.get_headers()
  if err == "truncated" then
      -- man kann wählen, die aktuelle Anfrage zu ignorieren oder abzulehnen
  end
  for k, v in pairs(h) do
      ...
  end

Standardmäßig werden die ersten 100 Header zurückgegeben. Wenn die Anzahl 100 überschreitet, wird ein truncated-Fehler gemeldet, und es liegt am Entwickler, zu entscheiden, wie damit umgegangen wird. Sie fragen sich vielleicht, warum dies so gehandhabt wird, was ich später im Abschnitt über Sicherheitslücken erwähnen werde.

Wir sollten jedoch beachten, dass OpenResty keine spezifische API zum Abrufen eines bestimmten Anfrageheaders bietet, was bedeutet, dass es keine Form wie ngx.req.header['host'] gibt. Wenn Sie ein solches Bedürfnis haben, müssen Sie sich auf die NGINX-Variable $http_xxx verlassen, um dies zu erreichen. In OpenResty können Sie dies dann über ngx.var.http_xxx erhalten.

Schauen wir uns nun an, wie wir den Anfrageheader umschreiben und löschen sollten. Die APIs für beide Operationen sind recht intuitiv:

ngx.req.set_header("Content-Type", "text/css")
ngx.req.clear_header("Content-Type")

Natürlich erwähnt die offizielle Dokumentation auch andere Methoden zum Entfernen des Anfrageheaders, wie das Setzen des Werts des Headers auf nil usw. Ich empfehle jedoch immer noch, clear_header zu verwenden, um dies einheitlich für die Klarheit des Codes zu tun.

Anfragekörper

Schließlich schauen wir uns den Anfragekörper an. Aus Leistungsgründen liest OpenResty den Anfragekörper nicht aktiv, es sei denn, Sie erzwingen die Aktivierung der lua_need_request_body-Direktive in nginx.conf. Darüber hinaus speichert OpenResty für größere Anfragekörper den Inhalt in einer temporären Datei auf der Festplatte, sodass der gesamte Prozess des Lesens des Anfragekörpers wie folgt aussieht.

ngx.req.read_body()
local data = ngx.req.get_body_data()
if not data then
    local tmp_file = ngx.req.get_body_file()
     -- io.open(tmp_file)
     -- ...
 end

Dieser Code enthält eine IO-blockierende Operation zum Lesen der Festplattendatei. Sie sollten die Konfiguration von client_body_buffer_size (standardmäßig 16 KB auf 64-Bit-Systemen) anpassen, um die blockierenden Operationen zu minimieren; Sie können auch client_body_buffer_size und client_max_body_size gleich konfigurieren und sie vollständig im Speicher handhaben, abhängig von der Größe Ihres Speichers und der Anzahl der gleichzeitigen Anfragen, die Sie bearbeiten.

Darüber hinaus kann der Anfragekörper umgeschrieben werden. Die beiden APIs ngx.req.set_body_data und ngx.req.set_body_file akzeptieren eine Zeichenkette und eine lokale Festplattendatei als Eingabeparameter, um das Umschreiben des Anfragekörpers zu ermöglichen. Diese Art von Operation ist jedoch ungewöhnlich, und Sie können die Dokumentation für weitere Details überprüfen.

Antwort

Nach der Verarbeitung der Anfrage müssen wir eine Antwort an den Client senden. Wie die Anfragenachricht besteht auch die Antwortnachricht aus mehreren Teilen: der Statuszeile, den Antwortheadern und dem Antwortkörper. Ich werde die entsprechenden APIs nach diesen drei Teilen vorstellen.

Statuszeile

Das Hauptaugenmerk in der Statuszeile liegt auf dem Statuscode. Standardmäßig ist der zurückgegebene HTTP-Statuscode 200, was der in OpenResty eingebauten Konstante ngx.HTTP_OK entspricht. Aber in der Welt des Codes ist es immer der Code, der die meisten Ausnahmefälle behandelt.

Wenn Sie die Anfragenachricht überprüfen und feststellen, dass es sich um eine bösartige Anfrage handelt, müssen Sie die Anfrage beenden:

ngx.exit(ngx.HTTP_BAD_REQUEST)

Es gibt jedoch eine besondere Konstante in den HTTP-Statuscodes von OpenResty: ngx.OK. Im Fall von ngx.exit(ngx.OK) verlässt die Anfrage die aktuelle Verarbeitungsphase und geht zur nächsten Phase über, anstatt direkt zum Client zurückzukehren.

Natürlich können Sie auch wählen, nicht zu beenden und den Statuscode einfach mit ngx.status umzuschreiben, wie im folgenden Code geschrieben.

ngx.status = ngx.HTTP_FORBIDDEN

Sie können sie in der Dokumentation nachschlagen, wenn Sie mehr über die Statuscode-Konstanten wissen möchten.

Antwortheader

In Bezug auf die Antwortheader gibt es zwei Möglichkeiten, sie einzurichten. Die erste ist die einfachste.

ngx.header.content_type = 'text/plain'
ngx.header["X-My-Header"] = 'blah blah'
ngx.header["X-My-Header"] = nil -- löschen

Hier enthält ngx.header die Informationen der Antwortheader, die gelesen, geändert und gelöscht werden können.

Die zweite Möglichkeit, die Antwortheader zu setzen, ist ngx_resp.add_header aus dem lua-resty-core-Repository, das eine Kopfzeilennachricht hinzufügt und wie folgt aufgerufen wird:

local ngx_resp = require "ngx.resp"
ngx_resp.add_header("Foo", "bar")

Der Unterschied zur ersten Methode besteht darin, dass add_header ein vorhandenes Feld mit demselben Namen nicht überschreibt.

Antwortkörper

Schließlich schauen wir uns den Antwortkörper an. In OpenResty können Sie ngx.say und ngx.print verwenden, um den Antwortkörper auszugeben.

ngx.say('hello, world')

Die Funktionalität der beiden APIs ist identisch, der einzige Unterschied besteht darin, dass ngx.say am Ende einen Zeilenumbruch hat.

Um die Ineffizienz der Zeichenkettenverkettung zu vermeiden, unterstützen ngx.say / ngx.print sowohl Zeichenketten als auch Array-Formate als Parameter.

$ resty -e 'ngx.say({"hello", ", ", "world"})'
 hello, world

Diese Methode überspringt die Zeichenkettenverkettung auf Lua-Ebene und überlässt sie den C-Funktionen.

Zusammenfassung

Lassen Sie uns den heutigen Inhalt überprüfen. Wir haben die OpenResty-APIs vorgestellt, die mit den Anfrage- und Antwortnachrichten verbunden sind. Wie Sie sehen können, ist die OpenResty-API flexibler und leistungsfähiger als die NGINX-Direktive.

Ist die von OpenResty bereitgestellte Lua-API also ausreichend, um Ihre Anforderungen bei der Verarbeitung von HTTP-Anfragen zu erfüllen? Bitte hinterlassen Sie Ihre Kommentare und teilen Sie diesen Artikel mit Ihren Kollegen und Freunden, damit wir uns austauschen und gemeinsam verbessern können.