Mit XProcs p:http-request auf APIs zugreifen

Dem Anlass eines grippalen Infektes angemessen, habe ich mich mit der API des Online-Virenscanners VirusTotal auseinander gesetzt. Resultat ist ein XProc-Step*, welcher mittels der VirusTotal-API Dateien zum VirusTotal-Web-Service schiebt und die Prüfergebnisse abholt.

An diesem Beispiel lässt sich zeigen, wie XProc mit anderen Anwendungen über das Web kommunizieren kann. Für das Aufrufen von Webseiten im Browser, wird das Hypertext Transfer Protocol (HTTP) verwendet. HTTP ist aber nicht auf das Abrufen von Webseiten beschränkt, sondern wird auch für die Kommunikation zwischen Systemen verwendet und bildet damit die Grundlage vieler web-basierter Anwendungsschnittstellen.

XProc implementiert eine HTTP-Abfrage mittels p:http-request. Mit dem folgenden Code lässt sich beispielsweise der RSS-Feed dieses Blogs abrufen:

<p:http-request>
  <p:input port="source">
    <p:inline>
      <c:request method="GET" href="http://xporc.net/?feed=rss"/>
    </p:inline>
  </p:input>
</p:http-request>

Die VirusTotal-API mit XProc zu implementieren, ist freilich etwas schwieriger. Für die folgenden Schritte muss jeweils ein p:http-request konstruiert werden.

  • Laden der Datei
  • Senden der Datei
  • Abfrage des Reports

Mit dem ersten HTTP-Request lädt man die Datei vom lokalen Dateisystem – als kleine Randnotiz: Da p:load nur XML-Dateien lädt, verwenden wir hier p:http-request, um auch binäre Dateien verarbeiten zu können. Nach dem Request wird die Datei base64-kodiert in ein c:body-Element geschachtelt.

Nun muss diese Datei zum Server geschickt werden. Neben der Datei erwartet der Server außerdem als Parameter den Dateinamen und einen API-Key, den man nach einer Registrierung bei VirusTotal kostenfrei erhält.

Um den eigentlichen Request zu schreiben, kann man sich mit der Kombination von p:in-scope-names und p:template das Leben einfacher machen. Mit p:in-scope-names werden alle im aktuellen Kontext verfügbaren Variablen und Parameter in ein Parameter-Dokument umgewandelt. Mit p:template kann man sich anschließend ein Skelett des Requests konstruieren und die Parameter automatisch einfügen.

<p:in-scope-names name="vars"/>

<p:template>
  <p:input port="template">
    <p:inline>
      <c:request method="POST" href="{$scan-url}">
        <c:header name="api-key" value="{$api-key}"/>
        <c:multipart content-type="multipart/form-data" 
          boundary="=-=-=-=-=">
          <c:body content-type="{$content-type}"
            disposition='form-data; name="file"; filename="{$href}"'
            >{/c:body/text()}</c:body>
        </c:multipart>
      </c:request>
    </p:inline>
  </p:input>
  <p:input port="source">
    <p:pipe port="result" step="get-local-file"/>
  </p:input>
  <p:input port="parameters">
    <p:pipe port="result" step="vars"/>
  </p:input>
</p:template>

<p:http-request/>

Die VirusTotal-API erwartet einen POST-Request, so wie er von einem HTML-Upload-Formular abgeschickt werden würde. Das XProc-Markup für HTTP-Requests verlangt ein c:request-Element. Dessen href-Attribut gibt die URL für den Request an und mit method lässt sich der Typ des Requests bestimmen.

Daher muss der content-type den Wert multipart/form-data enthalten. Als Header der Nachricht schicken wir unseren persönlichen API-Key mit. Der Multipart-Content-Typ enthält dann die eigentliche Datei, die wir aus dem vorherigen Schritt laden.

Viel Forschungszeit habe ich darauf verwendet, einen Weg zu finden, die benötigten Datei-Parameter zu senden. So muss name="file" unbedingt im disposition-Attribut vorhanden sein. Die doppelten Anführungszeichen innerhalb des Attributs kann man XML-konform verwenden, wenn man den ganzen Attributwert in einfache Anführungszeichen setzt.

Nach erfolgreichem Request erhält man vom VirusTotal-Server erstmal nur eine JSON-Datei zurück. Wenn man XML Calabash als XProc-Prozessor verwendet, kann man sich aber ganz leicht diese Datei in eine XML-Repräsentation umwandeln. Dafür muss man die Option -Xtransparent-json setzen und Calabash wandelt das JSON automatisch in eine XML-Repräsentation um.

{"scan_id": "073fb9c30802ff9ca80bbf6724dc593c58c747e507e53dcf2970895453431dea-1435828633",
"sha1": "b209c5f5e8de370128c979c844a85bafaa6e6e8e",
"resource": "073fb9c30802ff9ca80bbf6724dc593c58c747e507e53dcf2970895453431dea",
"response_code": 1,
"sha256": "073fb9c30802ff9ca80bbf6724dc593c58c747e507e53dcf2970895453431dea",
"permalink": "https://www.virustotal.com/file/073fb9c30802ff9ca80bbf6724dc593c58c747e507e53dcf2970895453431dea/analysis/1435828633/",
"md5": "f4ab484047849edd7b4b24c53516f18b",
"verbose_msg": "Scan request successfully queued, come back later for the report"}

Der Hash aus der Antwort wird zusammen mit dem API-Key für die Abfrage des Reports benötigt. Bei der Konstruktion des POST-Requests versieht man das c:body-Element mit dem content-type="application/x-www-form-urlencoded" und fügt darin die Parameter in der Form param=value ein.

<p:template>
  <p:input port="template">
    <p:inline>
      <c:request method="POST" href="{$report-url}">
        <c:body content-type="application/x-www-form-urlencoded">
          apikey={$api-key}&resource={/j:json/j:resource/text()}</c:body>
      </c:request>
    </p:inline>
  </p:input>
  <p:with-param name="report-url" select="$report-url"/>
  <p:with-param name="api-key" select="$api-key"/>
</p:template>

<p:http-request/>

Nach erfolgreicher Abfrage erhält man dann wieder ein JSON-Dokument mit dem Scan-Report. Über die transparent-json-Option nach XML transformiert, sieht das Ganze dann mit einigen Auslassungen („…“) wie folgt aus:

<j:json xmlns:j="http://marklogic.com/json" type="object">
  …
  <j:scans type="object">
    <j:AegisLab type="object">
      <j:result type="null"/>
      <j:detected type="boolean">false</j:detected>
      <j:update type="string">20150701</j:update>
      <j:version type="string">1.5</j:version>
    </j:AegisLab>
    <j:Tencent type="object">
      <j:result type="null"/>
      <j:detected type="boolean">false</j:detected>
      <j:update type="string">20150701</j:update>
      <j:version type="string">1.0.0.1</j:version>
    </j:Tencent>
    …
  </j:scans>
  …
</j:json>

Das Virenscanner-Beispiel demonstriert, wie man mit p:http-request über die HTTP-API mit einem System kommunizieren kann. Auch wenn ein „XProc-basierter Virenscanner“ sicher eher eine Spielerei ist, so ist die Kommunikation über web-basierte Schnittstellen heutzutage eine zwingende Anforderung. Für eine integrierte Verlagsproduktion ist es entscheidend, dass Daten und Metadaten mit beteiligten Systemen ausgetauscht werden können. Von daher bietet sich XProc mit dem p:http-request dafür an, nicht nur XML-Workflows, sondern auch deren Schnittstellen zu Web-Services abzubilden.

Anmerkungen

virustotal auf GitHub

Ein weiteres Beispiel ist der von meinem Kollegen Gerrit Imsieke entwickelte XProc-basierte Cross-Ref-DOI-Resolver

Kommentar schreiben