<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="de">
	<id>https://wiki.bergau.de/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=MINERVA-Rademacker</id>
	<title>OBS Wiki - Benutzerbeiträge [de]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.bergau.de/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=MINERVA-Rademacker"/>
	<link rel="alternate" type="text/html" href="https://wiki.bergau.de/Spezial:Beitr%C3%A4ge/MINERVA-Rademacker"/>
	<updated>2026-11-14T01:22:30Z</updated>
	<subtitle>Benutzerbeiträge</subtitle>
	<generator>MediaWiki 1.43.9</generator>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zugaenge&amp;diff=67340</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Zugaenge</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zugaenge&amp;diff=67340"/>
		<updated>2026-11-05T12:40:03Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Zugangscode fuer die OBS-Smart-App mit F6 ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Zugänge=&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Zugang&#039;&#039;&#039; (Account) ist der Kanal, über den ein externer Konsument den REST-Server anspricht. Er bündelt API-Key, Host-Beschränkung, CORS-Konfiguration, optionale JWT-Authentifizierung und optionale mTLS-Subject-Prüfung.&lt;br /&gt;
&lt;br /&gt;
==Aufruf==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Zugänge&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Felder==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld          !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Name          || Sprechender Name, erscheint im Protokoll und der Statistik&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv         || Inaktive Zugänge werden bei der Authentifizierung mit 403 abgewiesen&lt;br /&gt;
|-&lt;br /&gt;
| API-Key       || Eindeutiger Schlüssel, den der Konsument im HTTP-Header &#039;&#039;apikey&#039;&#039; senden muss. Per Schlüsselsymbol generierbar&lt;br /&gt;
|-&lt;br /&gt;
| Host          || Optionaler Hostname oder IP-Adresse des Konsumenten. Stimmt die anfragende IP nicht überein, wird mit 403 abgelehnt&lt;br /&gt;
|-&lt;br /&gt;
| CORS-Origins  || Liste erlaubter Origins für Browser-Aufrufe, mehrere durch &#039;&#039;;&#039;&#039; getrennt. &#039;&#039;&#039;Leer heisst nicht „keine Einschränkung&amp;quot;, sondern „kein Browser-Zugriff&amp;quot;&#039;&#039;&#039; - siehe unten&lt;br /&gt;
|-&lt;br /&gt;
| mTLS-Subject  || Erwartete RDN-Komponente(n) im Client-Zertifikat-Subject, Komma-/Semikolon-getrennt (alle müssen vorkommen; nur sinnvoll bei mTLS-Servern)&lt;br /&gt;
|-&lt;br /&gt;
| JWT           || Schalter, ob für diesen Zugang ein JWT-Token nötig ist&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Endpunkt  || Pfad, über den &#039;&#039;&#039;alle Token-Vorgänge&#039;&#039;&#039; laufen: Anmelden, Erneuern und Abmelden (z.B. &#039;&#039;oauth&#039;&#039; oder &#039;&#039;meineapp/dev/auth&#039;&#039;). Verglichen wird der &#039;&#039;&#039;vollständige Pfad&#039;&#039;&#039;, mehrstufige Angaben mit &#039;&#039;/&#039;&#039; sind also erlaubt&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Key       || Geheimer Schlüssel zur Signierung/Verifikation, per Schlüsselsymbol generierbar&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Exp       || Gültigkeitsdauer eines ausgestellten Access-Tokens in &#039;&#039;&#039;Minuten&#039;&#039;&#039;. Derselbe Wert wird dem Konsumenten in der Token-Antwort als &#039;&#039;expiresIn&#039;&#039; in &#039;&#039;&#039;Sekunden&#039;&#039;&#039; mitgeteilt und steht dem Endpunkt-Skript als &#039;&#039;_OBS_JWT_EXP_SEC&#039;&#039; zur Verfügung&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==API-Key==&lt;br /&gt;
&lt;br /&gt;
Der API-Key ist die Basis-Authentifizierung. Der Client übergibt ihn im HTTP-Header:&lt;br /&gt;
&lt;br /&gt;
 apikey: abcd-efgh-ijkl-mnop-qrstuvwxyz12&lt;br /&gt;
&lt;br /&gt;
Wird ein Aufruf ohne &#039;&#039;apikey&#039;&#039;-Header gestellt, sucht der Server intern nach einem Zugang mit API-Key &#039;&#039;&#039;*&#039;&#039;&#039; - damit lassen sich (mit Vorsicht!) auch öffentliche Endpunkte ohne Authentifizierung realisieren.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein Zugang mit API-Key &#039;&#039;*&#039;&#039; sollte nur für bewusst freigegebene Endpunkte verwendet werden und immer in Kombination mit einer Host-Beschränkung oder CORS-Beschränkung betrieben werden.}}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Nur für wirklich öffentliche Dateien.&#039;&#039;&#039; Ohne API-Key kennt der&lt;br /&gt;
Endpunkt weder Mandant noch Rolle - es gibt nichts, woran er eine Berechtigung&lt;br /&gt;
festmachen könnte ausser dem, was in der URL steht. Ein Wildcard-Zugang eignet&lt;br /&gt;
sich deshalb für Artefakte, die für jeden bestimmt sind (Test-APK, Handbuch),&lt;br /&gt;
&#039;&#039;&#039;nicht&#039;&#039;&#039; für Kundenbelege. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]].}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Bei einem Wildcard-Zugang sollte &#039;&#039;CORS-Origin&#039;&#039; &#039;&#039;&#039;leer&#039;&#039;&#039; bleiben.&lt;br /&gt;
Ein &#039;&#039;*&#039;&#039; dort wirkt auf die Preflight-Antwort &#039;&#039;&#039;aller&#039;&#039;&#039; Zugänge (siehe unten);&lt;br /&gt;
ein Download über die Adresszeile des Browsers sendet ohnehin kein &#039;&#039;Origin&#039;&#039;,&lt;br /&gt;
CORS greift dort also gar nicht.}}&lt;br /&gt;
&lt;br /&gt;
==Host-Beschränkung==&lt;br /&gt;
&lt;br /&gt;
Ist im Feld &#039;&#039;&#039;Host&#039;&#039;&#039; ein Wert hinterlegt, akzeptiert der Server Anfragen unter diesem Zugang nur dann, wenn die Remote-IP exakt diesem Host entspricht oder durch DNS-Auflösung auf diese IP zeigt. DNS-Auflösungen werden 5 Minuten gecached.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Hinter einem Reverse-Proxy ist die Remote-IP immer die des Proxys. Die Host-Beschränkung kann dort keine einzelnen Konsumenten unterscheiden. Der Header &#039;&#039;X-Forwarded-For&#039;&#039; wird dafür bewusst nicht ausgewertet, weil jeder Client ihn selbst setzen kann.}}&lt;br /&gt;
&lt;br /&gt;
==CORS==&lt;br /&gt;
&lt;br /&gt;
Soll ein Endpunkt aus einer Browser-Anwendung (Single-Page-App, JavaScript) heraus aufgerufen werden, muss das Origin der Anwendung in der CORS-Liste des Zugangs eingetragen sein.&lt;br /&gt;
&lt;br /&gt;
Format der Liste:&lt;br /&gt;
&lt;br /&gt;
 https://app.firma.de;https://test.firma.de;https://localhost:8080&lt;br /&gt;
&lt;br /&gt;
Ablauf:&lt;br /&gt;
&lt;br /&gt;
# Browser sendet einen Preflight-Request (&#039;&#039;OPTIONS&#039;&#039;). Der REST-Server prüft das Origin gegen die globale Origin-Liste &#039;&#039;&#039;aller&#039;&#039;&#039; Accounts (Cache, TTL 60s) und antwortet mit 204 + CORS-Header, falls erlaubt.&lt;br /&gt;
# Der eigentliche Request wird gegen die Origin-Liste &#039;&#039;&#039;des aktuell verwendeten Zugangs&#039;&#039;&#039; geprüft.&lt;br /&gt;
# Stimmt das Origin nicht, wird mit 403 &#039;&#039;Origin nicht erlaubt für Account ...&#039;&#039; abgelehnt.&lt;br /&gt;
&lt;br /&gt;
Der Preflight kann nicht zugangsbezogen prüfen: der Browser schickt dabei keinen&lt;br /&gt;
&#039;&#039;apikey&#039;&#039; mit, der Zugang ist zu diesem Zeitpunkt unbekannt. &#039;&#039;&#039;Die Durchsetzung&lt;br /&gt;
liegt deshalb auf Schritt 2&#039;&#039;&#039;, nicht auf dem Preflight. Praktische Folge: trägt&lt;br /&gt;
&#039;&#039;&#039;ein&#039;&#039;&#039; Zugang auf dem Server ein &#039;&#039;*&#039;&#039; in seiner Liste, antwortet jeder&lt;br /&gt;
Preflight mit &#039;&#039;Access-Control-Allow-Origin: *&#039;&#039; - auch für Zugänge mit engerer&lt;br /&gt;
Liste. Deren eigentliche Anfrage wird trotzdem gegen die eigene Liste geprüft.&lt;br /&gt;
&lt;br /&gt;
Erlaubte Methoden: &#039;&#039;GET, POST, PUT, DELETE, PATCH&#039;&#039; (systemseitig festgelegt).&lt;br /&gt;
&lt;br /&gt;
Bei den &#039;&#039;&#039;Anfrage-Headern&#039;&#039;&#039; spiegelt der Server im Preflight zurück, was der&lt;br /&gt;
Browser in &#039;&#039;Access-Control-Request-Headers&#039;&#039; angefragt hat. Damit sind eigene&lt;br /&gt;
Header eines Projekts - &#039;&#039;Idempotency-Key&#039;&#039;, &#039;&#039;If-Match&#039;&#039;, später&lt;br /&gt;
&#039;&#039;Upload-Offset&#039;&#039;/&#039;&#039;Upload-Length&#039;&#039; - ohne Änderung am Server nutzbar. Fragt der&lt;br /&gt;
Preflight keine Header an, antwortet der Server mit der Liste&lt;br /&gt;
&#039;&#039;Content-Type, apikey, Authorization, Accept&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Die Header-Liste ist &#039;&#039;&#039;keine Schutzgrenze&#039;&#039;&#039; - das ist das Origin. Sie&lt;br /&gt;
sagt dem Browser nur, welche Header der Server versteht. Dazu kommt, dass die&lt;br /&gt;
Zugangsdaten dieser Schnittstelle in Headern stehen (&#039;&#039;apikey&#039;&#039;,&lt;br /&gt;
&#039;&#039;Authorization&#039;&#039;) und nicht in Cookies: es gibt also keine automatisch&lt;br /&gt;
mitgeschickte Berechtigung, die eine fremde Seite ausnutzen könnte.}}&lt;br /&gt;
&lt;br /&gt;
===Was „leer&amp;quot; bedeutet===&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Ein &#039;&#039;&#039;leeres&#039;&#039;&#039; Feld CORS-Origins heisst &#039;&#039;&#039;nicht&#039;&#039;&#039; „keine&lt;br /&gt;
Einschränkung&amp;quot;. Es heisst: &#039;&#039;&#039;jeder Aufruf mit &#039;&#039;Origin&#039;&#039;-Header wird mit 403&lt;br /&gt;
abgelehnt&#039;&#039;&#039;. Für Clients &#039;&#039;&#039;ohne&#039;&#039;&#039; &#039;&#039;Origin&#039;&#039;-Header - mobile Apps, Server,&lt;br /&gt;
curl - ändert das Feld gar nichts, sie werden nie gegen CORS geprüft.}}&lt;br /&gt;
&lt;br /&gt;
Daraus ergeben sich drei sinnvolle Einstellungen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| leer || Kein Browser-Zugriff. Richtig für Zugänge, die nur von Apps oder Servern benutzt werden&lt;br /&gt;
|-&lt;br /&gt;
| konkrete Origins || Browser-Zugriff nur von diesen Seiten. Der Regelfall für Web-Anwendungen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;*&#039;&#039; || Browser-Zugriff von überall. Wirkt zusätzlich auf die Preflight-Antwort aller anderen Zugänge - bewusst und sparsam verwenden&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Für eine Browser-Anwendung ist dieses Feld besonders wichtig, weil ihr API-Key&lt;br /&gt;
zwangsläufig im JavaScript liegt und damit öffentlich ist. Die Origin-Liste&lt;br /&gt;
verhindert dann, dass jemand den Key nimmt und von seiner eigenen Seite benutzt.&lt;br /&gt;
Sie ist allerdings kein Ersatz für eine Zugriffskontrolle: wer den Key hat,&lt;br /&gt;
umgeht CORS mit einem einzigen Aufruf ausserhalb des Browsers.&lt;br /&gt;
&lt;br /&gt;
==JWT-Authentifizierung==&lt;br /&gt;
&lt;br /&gt;
Wird ein Zugang mit aktiver JWT-Pflicht aufgerufen, sind die folgenden Phasen zu durchlaufen (Phase 3 nur bei genutztem Refresh).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Anmelden, Erneuern und Abmelden laufen über denselben Endpunkt&#039;&#039;&#039; - den im&lt;br /&gt;
Feld JWT-Endpunkt hinterlegten Pfad. Ein eigener Endpunkt fürs Abmelden ist&lt;br /&gt;
nicht nötig und wäre auch umständlicher: der JWT-Endpunkt greift &#039;&#039;&#039;vor&#039;&#039;&#039; der&lt;br /&gt;
Adressauflösung und braucht deshalb weder eine Zeile in &#039;&#039;RESTSRV_ENDPOINTS&#039;&#039;&lt;br /&gt;
noch eine Berechtigung in &#039;&#039;RESTSRV_ACCESS&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Unterschieden wird nach Verb und Token-Art:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Ablauf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; ohne &#039;&#039;Authorization&#039;&#039; || Anmelden (Skript-Methode &#039;&#039;Authenticate&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; mit Bearer, Refresh-Token || Erneuern (Skript-Methode &#039;&#039;Refresh&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;DELETE&#039;&#039; mit Bearer, Access-Token || Abmelden (kein Skript nötig)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; mit Bearer, Access-Token || &#039;&#039;&#039;401&#039;&#039;&#039; - unverändert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Dass das Abmelden am Verb hängt und nicht allein an der Token-Art, ist&lt;br /&gt;
Absicht. Würde ein vorgelegtes Access-Token beim &#039;&#039;POST&#039;&#039; als Abmeldung gedeutet,&lt;br /&gt;
würde ein Client, der seine beiden Token verwechselt, den Anwender still&lt;br /&gt;
abmelden - statt das klare 401 zu bekommen, an dem der Fehler auffällt.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 1: Token-Bezug===&lt;br /&gt;
&lt;br /&gt;
Der Konsument ruft den JWT-Endpunkt des Zugangs auf, z.B.:&lt;br /&gt;
&lt;br /&gt;
 POST https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
 Body:   {&amp;quot;username&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;password&amp;quot;:&amp;quot;...&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Der Server führt das im Zugang hinterlegte &#039;&#039;&#039;Authentifizierungs-Skript&#039;&#039;&#039; aus (F7 in der Zugangs-Liste). Das Skript muss eine Prozedur &#039;&#039;Authenticate&#039;&#039; bereitstellen, die über den Writer schreibt:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;status&#039;&#039; = &#039;&#039;1&#039;&#039; bei Erfolg, &#039;&#039;9&#039;&#039; bei Misserfolg&lt;br /&gt;
* &#039;&#039;error&#039;&#039; = Fehlertext (bei status=9)&lt;br /&gt;
* &#039;&#039;_OBS_JWT_ID&#039;&#039;, &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039;, &#039;&#039;_OBS_JWT_AUDIENCE&#039;&#039; für die JWT-Claims (optional)&lt;br /&gt;
* &#039;&#039;_OBS_JWT_CLAIM_&amp;amp;lt;name&amp;amp;gt;&#039;&#039; für beliebige Custom-Claims (z.B. Mandant &#039;&#039;tenant&#039;&#039;, Rollen &#039;&#039;roles&#039;&#039;); im Folge-Skript lesbar&lt;br /&gt;
* &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039; (optional) löst die Ausstellung eines Refresh-Tokens aus; &#039;&#039;_OBS_JWT_REFRESH_EXP&#039;&#039; setzt dessen Lebensdauer in Minuten (Default 90 Tage)&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Die Lebensdauer eines Refresh-Tokens ist nach oben auf &#039;&#039;&#039;372 Tage&#039;&#039;&#039;&lt;br /&gt;
begrenzt. Ein mit &#039;&#039;_OBS_JWT_REFRESH_EXP&#039;&#039; länger ausgestelltes Token wird bei der&lt;br /&gt;
Vorlage &#039;&#039;&#039;nicht mehr verifiziert&#039;&#039;&#039; und mit 401 abgewiesen - der Fehler fällt&lt;br /&gt;
also erst Monate später auf, beim ersten Erneuerungsversuch.}}&lt;br /&gt;
&lt;br /&gt;
Die Signatur lautet &#039;&#039;procedure Authenticate(oReader: TxRestReader; oWriter: TxRestWriter);&#039;&#039; - Details unter [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]]. Bei Erfolg signiert der Server einen Token (HS256, Issuer &#039;&#039;OBS REST-Server&#039;&#039;, Lebenszeit gemäß JWT-Exp) und antwortet:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;token&amp;quot;:        &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;accessToken&amp;quot;:  &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;:    28800,&lt;br /&gt;
  &amp;quot;serverTime&amp;quot;:   &amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Zum Aufbau der Antwort:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;token&#039;&#039; und &#039;&#039;accessToken&#039;&#039; enthalten &#039;&#039;&#039;denselben&#039;&#039;&#039; Token. &#039;&#039;accessToken&#039;&#039; ist der Name, den die meisten Client-Bibliotheken erwarten; &#039;&#039;token&#039;&#039; bleibt für bestehende Konsumenten erhalten.&lt;br /&gt;
* &#039;&#039;expiresIn&#039;&#039; ist die Gültigkeit in &#039;&#039;&#039;Sekunden&#039;&#039;&#039; (JWT-Exp × 60).&lt;br /&gt;
* &#039;&#039;&#039;Alle weiteren Felder, die das Authenticate-Skript zurückgibt, werden mit ausgeliefert&#039;&#039;&#039; - mit Ausnahme von &#039;&#039;status&#039;&#039; und den &#039;&#039;_OBS_&#039;&#039;-Steuerfeldern. So kann das Skript z.B. eine Benutzer-Id, den Mandanten oder Rollen direkt in die Anmelde-Antwort legen, ohne dass der Client dafür einen zweiten Aufruf braucht.&lt;br /&gt;
&lt;br /&gt;
Beispiel mit Zusatzfeldern aus dem Skript:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;benutzerId&amp;quot;: &amp;quot;4711&amp;quot;, &amp;quot;tenant&amp;quot;: &amp;quot;nord&amp;quot;, &amp;quot;roles&amp;quot;: [&amp;quot;TECHNIKER&amp;quot;],&lt;br /&gt;
  &amp;quot;token&amp;quot;: &amp;quot;eyJ...&amp;quot;, &amp;quot;accessToken&amp;quot;: &amp;quot;eyJ...&amp;quot;, &amp;quot;refreshToken&amp;quot;: &amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;: 28800, &amp;quot;serverTime&amp;quot;: &amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Mit der Ausstellung legt der Server zu dem Token-Paar eine &#039;&#039;&#039;Sitzungszeile&#039;&#039;&#039; in&lt;br /&gt;
&#039;&#039;RESTSRV_TOKEN&#039;&#039; an. Lässt sich diese Zeile nicht schreiben (Tabelle fehlt,&lt;br /&gt;
Datenbankproblem), wird &#039;&#039;&#039;kein Token ausgeliefert&#039;&#039;&#039;: der Server antwortet mit&lt;br /&gt;
&#039;&#039;&#039;503&#039;&#039;&#039; und &#039;&#039;Retry-After&#039;&#039;. Das ist bewusst von der abgelehnten Anmeldung&lt;br /&gt;
unterschieden - die Zugangsdaten waren in Ordnung, der Versuch darf wiederholt&lt;br /&gt;
werden.&lt;br /&gt;
&lt;br /&gt;
Wird die Anmeldung abgelehnt (&#039;&#039;status&#039;&#039; = 9), antwortet der Server mit&lt;br /&gt;
&#039;&#039;&#039;401 Unauthorized&#039;&#039;&#039;, dem Code &#039;&#039;AUTH_FAILED&#039;&#039; und reicht den Text aus dem Feld&lt;br /&gt;
&#039;&#039;error&#039;&#039; als &#039;&#039;error.message&#039;&#039; an den Client durch - der Konsument kann ihn also&lt;br /&gt;
direkt anzeigen. Die Formulierung liegt damit beim Skript.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;AUTH_FAILED&#039;&#039; unterscheidet die &#039;&#039;&#039;abgelehnte Anmeldung&#039;&#039;&#039; vom&lt;br /&gt;
&#039;&#039;AUTH_EXPIRED&#039;&#039;, das ein abgelaufenes oder gesperrtes Token liefert. Ein Client&lt;br /&gt;
sollte auf den Code reagieren und nicht auf den Text: Bei &#039;&#039;AUTH_FAILED&#039;&#039; gehört&lt;br /&gt;
die Anmeldemaske noch einmal vorgelegt, bei &#039;&#039;AUTH_EXPIRED&#039;&#039; erst ein Refresh&lt;br /&gt;
versucht.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der durchgereichte Text ist für den Endanwender gedacht und sollte&lt;br /&gt;
keine internen Details verraten. „Benutzer oder Passwort ist falsch&amp;quot; ist eine gute&lt;br /&gt;
Meldung, „Kein Datensatz in BENUTZ mit b_name=meier&amp;quot; nicht.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 2: Token-Verwendung===&lt;br /&gt;
&lt;br /&gt;
Bei allen folgenden Aufrufen sendet der Client den Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 GET https://api.meinserver.de/kalender/v1&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi...&lt;br /&gt;
&lt;br /&gt;
Der Server verifiziert Signatur, Issuer, Ausstellzeitpunkt und Ablauf (Toleranz: 20 Sekunden Clock-Skew) und prüft anschliessend die &#039;&#039;&#039;Sitzung&#039;&#039;&#039; in &#039;&#039;RESTSRV_TOKEN&#039;&#039;. Im Skript stehen die Claims als Parameter &#039;&#039;_OBS_JWT_ID&#039;&#039;, &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039; und &#039;&#039;_OBS_JWT_AUDIENCE&#039;&#039; zur Verfügung.&lt;br /&gt;
&lt;br /&gt;
Die Sitzungsprüfung weist ein Token ab, das zu keiner Zeile gehört oder dessen&lt;br /&gt;
Sitzung gesperrt wurde - beides mit &#039;&#039;&#039;401&#039;&#039;&#039; und dem Code &#039;&#039;AUTH_EXPIRED&#039;&#039;. Der&lt;br /&gt;
Grund steht jeweils im Protokoll &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; unter &#039;&#039;TokenStore: ...&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein Token, das &#039;&#039;&#039;vor&#039;&#039;&#039; der Einführung dieser Prüfung ausgestellt&lt;br /&gt;
wurde, gehört zu keiner Sitzung und wird abgewiesen. Nach dem Update müssen sich&lt;br /&gt;
angemeldete Konsumenten also einmalig neu anmelden.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 3: Token erneuern (Refresh)===&lt;br /&gt;
&lt;br /&gt;
Soll die App lange ohne Neuanmeldung arbeiten, stellt das Authenticate-Skript zusätzlich ein Refresh-Token aus (Feld &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039;). Zum Erneuern ruft der Client &#039;&#039;&#039;denselben JWT-Endpunkt&#039;&#039;&#039; auf, aber mit dem Refresh-Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 POST https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi... (Refresh-Token)&lt;br /&gt;
&lt;br /&gt;
Der Server erkennt am Bearer-Token den Refresh-Fall, verifiziert das Token (Signatur, Ablauf, &#039;&#039;token_use=refresh&#039;&#039;), &#039;&#039;&#039;entwertet es in der Sitzung&#039;&#039;&#039; und ruft erst dann im selben Skript die Methode &#039;&#039;&#039;Refresh&#039;&#039;&#039; auf. Das Skript liefert nur noch die aktuellen Claims zurück - Rotation, Einmalgebrauch und Sperrliste erledigt der Server. Die Antwort ist ein rotiertes Paar &#039;&#039;{&amp;quot;token&amp;quot;,&amp;quot;accessToken&amp;quot;,&amp;quot;refreshToken&amp;quot;,&amp;quot;expiresIn&amp;quot;,&amp;quot;serverTime&amp;quot;}&#039;&#039; in &#039;&#039;&#039;derselben Sitzung&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Eine &#039;&#039;&#039;eigene Sperrtabelle im Skript ist nicht mehr nötig&#039;&#039;&#039; und sollte&lt;br /&gt;
entfernt werden. Wer sie weiterführt, rotiert zweimal: einmal im Skript, einmal im&lt;br /&gt;
Server. Der Server sieht die Skript-Tabelle nicht, und das Skript sieht die&lt;br /&gt;
Sitzung nicht.}}&lt;br /&gt;
&lt;br /&gt;
Verhalten bei einem bereits benutzten Refresh-Token:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Lage !! Antwort !! Wirkung auf die Sitzung&lt;br /&gt;
|-&lt;br /&gt;
| Erneute Vorlage &#039;&#039;&#039;innerhalb von 60 Sekunden&#039;&#039;&#039; || 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; || Keine. Das ist der Normalfall einer App, deren parallele Anfragen gleichzeitig in den Refresh laufen - eine gewinnt, die anderen sollen die vom Gewinner gespeicherten Token benutzen&lt;br /&gt;
|-&lt;br /&gt;
| Erneute Vorlage &#039;&#039;&#039;später&#039;&#039;&#039; || 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; || Die &#039;&#039;&#039;gesamte Sitzung wird gesperrt&#039;&#039;&#039;. Das Token wurde kopiert oder wiederholt, ohne dass ein Gewinner es noch brauchen könnte; der Vorfall wird mit IP protokolliert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Details und ein vollständiges Beispiel siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
===Phase 4: Abmelden (Logout)===&lt;br /&gt;
&lt;br /&gt;
Abgemeldet wird mit einem &#039;&#039;&#039;DELETE&#039;&#039;&#039; auf denselben JWT-Endpunkt, das&lt;br /&gt;
Access-Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 DELETE https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi... (Access-Token)&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 204 No Content&lt;br /&gt;
&lt;br /&gt;
Der Server sperrt die &#039;&#039;&#039;ganze Sitzung&#039;&#039;&#039; - also auch die noch nicht&lt;br /&gt;
abgelaufenen Access-Token aller vorherigen Erneuerungen. &#039;&#039;&#039;Ein Skript ist dafür&lt;br /&gt;
nicht nötig&#039;&#039;&#039;: es gibt keine Methode &#039;&#039;Logout&#039;&#039;, der Vorgang läuft komplett in&lt;br /&gt;
der Engine. Damit funktioniert das Abmelden für jeden Zugang mit aktiver&lt;br /&gt;
JWT-Pflicht, ohne dass am Authentifizierungs-Skript etwas geändert wird.&lt;br /&gt;
&lt;br /&gt;
* Ein wiederholter Aufruf ist unschädlich und bleibt &#039;&#039;&#039;204&#039;&#039;&#039; - auch wenn die Sitzung längst gesperrt ist.&lt;br /&gt;
* Ein abgelaufenes oder ungültiges Token ergibt &#039;&#039;&#039;401&#039;&#039;&#039; (es wird schon bei der Prüfung abgewiesen).&lt;br /&gt;
* Ein Refresh-Token statt des Access-Tokens ergibt &#039;&#039;&#039;401&#039;&#039;&#039;.&lt;br /&gt;
* Lässt sich die Sitzung nicht sperren, antwortet der Server &#039;&#039;&#039;503&#039;&#039;&#039; mit &#039;&#039;Retry-After&#039;&#039; - ein Client darf nicht glauben, er sei abgemeldet, während sein Token weiterläuft.&lt;br /&gt;
&lt;br /&gt;
====Alle Geräte abmelden====&lt;br /&gt;
&lt;br /&gt;
Der Fall „Gerät verloren&amp;quot; ist bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; am JWT-Endpunkt untergebracht:&lt;br /&gt;
er braucht eine fachliche Entscheidung darüber, wer das darf. Dafür gibt es das&lt;br /&gt;
&#039;&#039;oWriter.JwtRevoke&#039;&#039;, das jeder gewöhnliche Endpunkt aufrufen kann:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    // &#039;session&#039; = nur diese Sitzung, &#039;all&#039; = alle Sitzungen des Subjects&lt;br /&gt;
    oWriter.JwtRevoke(&#039;all&#039;);&lt;br /&gt;
    oWriter.Status(204);&lt;br /&gt;
    oWriter.NoBody();&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;all&#039;&#039; setzt voraus, dass das Authenticate-Skript &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039;&lt;br /&gt;
gesetzt hat. Ohne Subject lässt sich die Menge der Sitzungen nicht bestimmen, und&lt;br /&gt;
der Aufruf antwortet mit 503.}}&lt;br /&gt;
&lt;br /&gt;
==Woher ein Anmelder seine Identität bekommt==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Diese Frage entscheidet das Skript des Zugangs, nicht der Server.&#039;&#039;&#039; Das&lt;br /&gt;
Authentifizierungs-Skript liegt je Zugang in &amp;lt;code&amp;gt;ra_jwt_script&amp;lt;/code&amp;gt; - und&lt;br /&gt;
damit gilt für jede Anwendung ihre eigene Regel.&lt;br /&gt;
&lt;br /&gt;
Das ist keine Kleinigkeit, sondern der Punkt, an dem sich zwei Anwendungen&lt;br /&gt;
unterscheiden. Ein Beispiel aus dem Betrieb: Ein Zugang für eine&lt;br /&gt;
Techniker-Anwendung lehnt jeden Benutzer ohne Mitarbeiterzuordnung&lt;br /&gt;
(&amp;lt;code&amp;gt;BENUTZ.b_mitarbeiter&amp;lt;/code&amp;gt;) ab - &#039;&#039;&#039;bewusst&#039;&#039;&#039;, denn ein Techniker ohne&lt;br /&gt;
Zuordnung bekäme eine leere Auftragsliste und würde den Fehler bei sich suchen.&lt;br /&gt;
Für einen Kundenzugang wäre genau dieselbe Regel falsch: sie träfe &#039;&#039;&#039;jeden&#039;&#039;&#039;&lt;br /&gt;
Anmelder.&lt;br /&gt;
&lt;br /&gt;
Wer einen zweiten Zugang für eine andere Anwendung anlegt, prüft deshalb als&lt;br /&gt;
Erstes: Woraus besteht die Identität dieser Anmelder, und welche Ablehnung ist&lt;br /&gt;
für sie richtig? Ein kopiertes Skript bringt die Regel der anderen Anwendung&lt;br /&gt;
mit, und sie fällt erst auf, wenn sich der erste echte Anmelder nicht anmelden&lt;br /&gt;
kann.&lt;br /&gt;
&lt;br /&gt;
==Sitzungen (RESTSRV_TOKEN)==&lt;br /&gt;
&lt;br /&gt;
Je ausgestelltem Token-Paar entsteht eine Zeile. Sie enthält die Sitzung, die&lt;br /&gt;
Zeilenkennung, das Subject, die beiden Laufzeiten, IP und Trace-ID der Ausstellung&lt;br /&gt;
sowie die Kennzeichen „verbraucht&amp;quot; und „gesperrt&amp;quot;. Schickt ein Reverse-Proxy den Header&lt;br /&gt;
&#039;&#039;X-Forwarded-For&#039;&#039; mit, steht dessen Wert zusätzlich in &#039;&#039;rt_ip_fwd&#039;&#039;; &#039;&#039;rt_ip&#039;&#039; bleibt&lt;br /&gt;
die IP-Adresse der Verbindung.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Rotation und Widerruf sind getrennt.&#039;&#039;&#039; Ein Refresh entwertet nur das Refresh-Token; der zugehörige Access-Token bleibt bis zu seinem Ablauf gültig. Sonst bekämen parallel laufende Anfragen ein 401, obwohl niemand abgemeldet ist.&lt;br /&gt;
* &#039;&#039;&#039;Die Sitzung überlebt die Rotation.&#039;&#039;&#039; Alle Paare einer Anmeldung tragen dieselbe Sitzungskennung - deshalb kann ein Logout sie gemeinsam sperren.&lt;br /&gt;
* &#039;&#039;&#039;Aufräumen läuft automatisch&#039;&#039;&#039; beim Serverstart und danach höchstens stündlich. Gelöscht wird nur, was nichts mehr entscheiden kann: Zeilen, deren Access- &#039;&#039;&#039;und&#039;&#039;&#039; Refresh-Laufzeit vorbei sind. Eine verbrauchte Zeile bleibt bis dahin stehen, weil sonst die Erkennung eines später vorgelegten gestohlenen Refresh-Tokens verloren ginge.&lt;br /&gt;
* &#039;&#039;&#039;Kein Eingriff nötig.&#039;&#039;&#039; Die Tabelle wird nicht gepflegt; sie ist Diagnose-Material. Alle Abweisungen stehen als &#039;&#039;TokenStore: ...&#039;&#039; in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==mTLS-Subject-Prüfung==&lt;br /&gt;
&lt;br /&gt;
Wird im Feld &#039;&#039;&#039;mTLS-Subject&#039;&#039;&#039; ein Wert hinterlegt, prüft der Server zusätzlich zur Standard-mTLS-Validierung, ob der Subject-DN des (CA-validierten) Client-Zertifikats die hinterlegten &#039;&#039;&#039;RDN-Komponenten&#039;&#039;&#039; enthält. Verglichen wird jede Komponente &#039;&#039;&#039;vollständig&#039;&#039;&#039; (kein Substring-Match) und case-insensitiv; mehrere Komponenten werden durch Komma oder Semikolon getrennt und müssen &#039;&#039;&#039;alle&#039;&#039;&#039; im Subject vorkommen.&lt;br /&gt;
&lt;br /&gt;
Beispiele für Subject-Strings im Cert (OneLine-DN):&lt;br /&gt;
&lt;br /&gt;
 /C=DE/O=Firma GmbH/CN=client-prod.firma.de&lt;br /&gt;
 /C=DE/O=Firma GmbH/CN=client-test.firma.de&lt;br /&gt;
&lt;br /&gt;
Mit &#039;&#039;&#039;CN=client-prod.firma.de&#039;&#039;&#039; im Feld wird genau das prod-Cert akzeptiert, das test-Cert mit 401 abgewiesen. Da jede RDN-Komponente vollständig verglichen wird, matcht z.B. &#039;&#039;&#039;CN=client1&#039;&#039;&#039; nicht fälschlich auch &#039;&#039;&#039;CN=client10&#039;&#039;&#039;. Mehrere Bedingungen lassen sich mit Komma/Semikolon kombinieren, z.B. &#039;&#039;O=Firma GmbH;CN=client-prod.firma.de&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==Zugangscode für die App==&lt;br /&gt;
&lt;br /&gt;
Ein Gerät mit einer OBS-Smart-App braucht drei Angaben: die Adresse des REST-Servers, den API-Key des Zugangs und - in internen Netzen - den Zertifikats-Pin. OBS fasst sie zu einem &#039;&#039;&#039;Zugangscode&#039;&#039;&#039; zusammen, den die App als QR-Code einliest.&lt;br /&gt;
&lt;br /&gt;
Markieren Sie den Zugang in der Liste und drücken Sie {{F6}}. Der Dialog belegt alle Felder aus der vorhandenen Einrichtung vor; überschreiben können Sie jedes davon.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld !! Woher der Vorschlag kommt&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;App&#039;&#039;&#039; || Erstes Segment des Endpunkt-Pfades, zum Beispiel &#039;&#039;service&#039;&#039;. Sind dem Zugang Endpunkte mehrerer Apps zugeordnet, weist der Dialog darauf hin&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Host (FQDN)&#039;&#039;&#039; || Zertifikatsname des Server-Profils. Hier gehört die Adresse hinein, unter der das &#039;&#039;&#039;Gerät&#039;&#039;&#039; den Server erreicht - nicht die, auf der der Dienst lauscht&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Port&#039;&#039;&#039; || Standard-Bindung des Server-Profils, sonst die erste aktive&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;HTTPS&#039;&#039;&#039; || Aus dem Server-Profil&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Anzeigename&#039;&#039;&#039; || Name des Zugangs. Er erscheint in der App als Bezeichnung der Verbindung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;API-Key&#039;&#039;&#039; || Der API-Key des Zugangs&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Zertifikats-Pin&#039;&#039;&#039; || Nur, wenn das Server-Profil einen führt, siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile#Eigenes Zertifikat und Pin|Eigenes Zertifikat und Pin]]&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Alter Key noch Tage&#039;&#039;&#039; || Nur beim Schlüsseltausch: wie lange der bisherige API-Key daneben gültig bleibt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Hinweise&#039;&#039;&#039; sammelt der Dialog, was fehlt oder fragwürdig ist - etwa ein Host, der auf den Server selbst zeigt und vom Gerät aus nicht erreichbar wäre. Solange eine Pflichtangabe fehlt, entsteht kein Code.&lt;br /&gt;
&lt;br /&gt;
===Tasten im Dialog===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Taste !! Funktion&lt;br /&gt;
|-&lt;br /&gt;
| {{F2}} || QR-Code anzeigen, zum Abfotografieren mit dem Gerät&lt;br /&gt;
|-&lt;br /&gt;
| {{F3}} || Schlüssel tauschen: OBS erzeugt einen neuen API-Key. Der bisherige bleibt so viele Tage gültig, wie unter &#039;&#039;&#039;Alter Key noch Tage&#039;&#039;&#039; eingetragen sind - in dieser Zeit lesen die Geräte den neuen Code ein, ohne dass der Betrieb steht&lt;br /&gt;
|-&lt;br /&gt;
| {{F4}} || Zugangscode in die Zwischenablage&lt;br /&gt;
|-&lt;br /&gt;
| {{Key|Esc}} || Dialog schliessen&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Der Zugangscode enthält den API-Key im Klartext. Geben Sie ihn nur an das Gerät weiter, für das er bestimmt ist, und verschicken Sie ihn nicht über offene Kanäle.}}&lt;br /&gt;
&lt;br /&gt;
==Listen-Funktionen==&lt;br /&gt;
&lt;br /&gt;
In der Zugänge-Liste (allein):&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfg&#039;&#039;&#039; - neuer Zugang&lt;br /&gt;
* &#039;&#039;&#039;Return&#039;&#039;&#039; - Zugang bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F4&#039;&#039;&#039; - Sortierung&lt;br /&gt;
* &#039;&#039;&#039;F6&#039;&#039;&#039; - Zugangscode für eine OBS-Smart-App&lt;br /&gt;
* &#039;&#039;&#039;F7&#039;&#039;&#039; - JWT-Authentifizierungs-Skript bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F8&#039;&#039;&#039; - Statistik für den Zugang&lt;br /&gt;
&lt;br /&gt;
Aus der Endpunkt-Liste via F6 geöffnet (Auswahl für Berechtigung):&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;F2&#039;&#039;&#039; - markierte Zugänge dem Endpunkt zuordnen&lt;br /&gt;
* &#039;&#039;&#039;F5&#039;&#039;&#039; - Zugang in der Liste markieren / Markierung aufheben&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zertifikatserneuerung&amp;diff=67339</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zertifikatserneuerung&amp;diff=67339"/>
		<updated>2026-11-05T12:40:03Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Eigenes Zertifikat als dritter Weg; Pin als Voraussetzung ausgeschlossen [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
{{Seitentyp|Anleitung}}&lt;br /&gt;
= Zertifikatserneuerung =&lt;br /&gt;
&lt;br /&gt;
Der REST-Server kann sein TLS-Zertifikat selbst bei der Zertifizierungsstelle Let&#039;s Encrypt beantragen und vor Ablauf erneuern. Zertifikat und privater Schlüssel entstehen auf Ihrem Server und verlassen ihn nicht. Diese Seite beschreibt, wie Sie die Erneuerung je nach Netzaufbau einrichten.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Jedes Server-Profil braucht im Betrieb ein Zertifikat, auch hinter einem Reverse-Proxy und im VPN: Eine App verbindet sich ausschliesslich per HTTPS, und ohne Zertifikat startet das Profil nicht. &#039;&#039;kein SSL&#039;&#039; ist nur für lokale Tests gedacht. Das Zertifikat beziehen Sie entweder automatisch wie hier beschrieben, oder Sie hinterlegen es von Hand im Server-Profil und tauschen es vor Ablauf. In einem internen Netz ohne eigene Domain stellt OBS auf Wunsch selbst eines aus, siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile#Eigenes Zertifikat und Pin|Eigenes Zertifikat und Pin]].}}&lt;br /&gt;
&lt;br /&gt;
{{Auf einen Blick|1=&lt;br /&gt;
* &#039;&#039;&#039;Zwei Prüfarten.&#039;&#039;&#039; http-01 prüft über Port 80, dns-01 über einen DNS-Eintrag und braucht keinen offenen Port.&lt;br /&gt;
* &#039;&#039;&#039;Selbsttest vorab.&#039;&#039;&#039; Vor dem ersten Zertifikat und danach wöchentlich prüft der Dienst den Weg über das Testsystem der Zertifizierungsstelle.&lt;br /&gt;
* &#039;&#039;&#039;Erneuerung ab 30 Tagen Restlaufzeit.&#039;&#039;&#039; Das bisherige Zertifikat bleibt als Rückfall erhalten.&lt;br /&gt;
* &#039;&#039;&#039;dns-01 braucht nur ausgehende Verbindungen&#039;&#039;&#039; und eignet sich damit für VPN und für Server ohne Portfreigabe.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Voraussetzungen ==&lt;br /&gt;
* Ein Name, auf den das Zertifikat lautet, zum Beispiel &#039;&#039;obs.firma.de&#039;&#039;. Platzhalter-Zertifikate (&#039;&#039;*.firma.de&#039;&#039;) werden nicht unterstützt.&lt;br /&gt;
* Ein Server-Profil mit TLS, also ohne Haken &#039;&#039;&#039;kein SSL&#039;&#039;&#039; und ohne Haken &#039;&#039;&#039;Eigenes Zertifikat (Pin)&#039;&#039;&#039; - der Pin und die automatische Erneuerung schliessen sich aus, weil jede Erneuerung einen neuen Schlüssel und damit einen neuen Pin erzeugt.&lt;br /&gt;
* Ausgehende HTTPS-Verbindungen zur Zertifizierungsstelle (&amp;lt;code&amp;gt;acme-v02.api.letsencrypt.org&amp;lt;/code&amp;gt; und für den Selbsttest &amp;lt;code&amp;gt;acme-staging-v02.api.letsencrypt.org&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Bei dns-01 zusätzlich ausgehende HTTPS-Verbindungen zur Schnittstelle des DNS-Anbieters bzw. zur acme-dns-Instanz.&lt;br /&gt;
&lt;br /&gt;
=== Welche Prüfart? ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Prüfart !! Funktioniert, wenn !! Aus dem Internet erreichbar sein muss&lt;br /&gt;
|-&lt;br /&gt;
| http-01 || der Name öffentlich auf diesen Server zeigt und Port 80 von aussen auf ihn weitergeleitet ist || Port 80&lt;br /&gt;
|-&lt;br /&gt;
| dns-01 || sich der DNS-Eintrag &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; über eine Schnittstelle schreiben lässt, direkt oder über einen CNAME || nichts&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Welche Prüfart zu Ihrer [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|öffentlichen Freigabe]] passt:&lt;br /&gt;
* &#039;&#039;&#039;Direkte Portfreigabe:&#039;&#039;&#039; http-01 oder dns-01. Mit dns-01 bleibt Port 80 geschlossen.&lt;br /&gt;
* &#039;&#039;&#039;Reverse-Proxy:&#039;&#039;&#039; Das öffentliche Zertifikat liegt am Proxy. Der REST-Server braucht trotzdem ein Zertifikat für die Strecke vom Proxy. Dafür genügt ein selbst signiertes Zertifikat oder eines Ihrer internen Zertifizierungsstelle, sofern der Proxy ihm vertraut. Es wird im Server-Profil hinterlegt und muss vor Ablauf getauscht werden.&lt;br /&gt;
* &#039;&#039;&#039;VPN:&#039;&#039;&#039; dns-01.&lt;br /&gt;
&lt;br /&gt;
=== Wege bei dns-01 ===&lt;br /&gt;
Bei dns-01 fragt die Zertifizierungsstelle den TXT-Eintrag &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; ab. Der Dienst schreibt ihn vor der Prüfung und löscht ihn danach wieder. Dafür gibt es drei Wege:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Weg !! Wann !! Einmalig beim DNS des Namens !! Einstellung &#039;&#039;&#039;DNS-Anbieter&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Direkt beim Anbieter || Die Zone des Namens liegt bei Hetzner, Cloudflare oder IONOS || nichts, Sie brauchen nur ein API-Token || &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| CNAME auf eine Zone bei einem Anbieter || Die Zone des Namens liegt bei einem anderen Anbieter, oder im REST-Server sollen keine Zugangsdaten zur eigenen Zone liegen || CNAME &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf einen Namen in einer Zone bei Hetzner, Cloudflare oder IONOS || &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| CNAME auf acme-dns || Eine acme-dns-Instanz steht zur Verfügung, im eigenen Betrieb oder bei einem Dienstleister || CNAME &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf die &#039;&#039;fulldomain&#039;&#039; des acme-dns-Kontos || &#039;&#039;acme-dns&#039;&#039;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim CNAME-Weg genügt beim DNS-Anbieter des Namens ein von Hand angelegter Eintrag, auch bei Anbietern ohne Schnittstelle. Der REST-Server bekommt dann nur Zugang zur Zone, auf die der CNAME zeigt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Nutzen Sie einen DNS-Anbieter, der hier nicht aufgeführt ist, wenden Sie sich an den OBS-Support. Bietet der Anbieter eine Schnittstelle (API) für DNS-Einträge, ist eine Anbindung in der Regel möglich. Bis dahin funktioniert der CNAME-Weg mit jedem Anbieter.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Namen von DynDNS-Diensten (zum Beispiel &#039;&#039;…myfritz.net&#039;&#039;) eignen sich in der Regel nicht als Zertifikatsname, weil sich dort keine eigenen Einträge anlegen lassen. Verwenden Sie einen Namen unter Ihrer eigenen Domain. Der DynDNS-Name bleibt davon unberührt, zum Beispiel für den Zugang zum VPN.}}&lt;br /&gt;
&lt;br /&gt;
== Vorgehen ==&lt;br /&gt;
=== Gemeinsame Einstellungen ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Wählen Sie &#039;&#039;&#039;Stammdaten → Z Weitere Stammdaten → REST-Server → Server&#039;&#039;&#039; und öffnen Sie das Server-Profil.&lt;br /&gt;
# Stellen Sie auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039;.&lt;br /&gt;
# Tragen Sie im Feld &#039;&#039;&#039;Zertifikatsname (FQDN)&#039;&#039;&#039; den Namen ein, auf den das Zertifikat lauten soll.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Kontakt bei der CA&#039;&#039;&#039; eine E-Mail-Adresse ein. Die Zertifizierungsstelle schickt dorthin Warnungen, wenn ein Zertifikat abzulaufen droht.&lt;br /&gt;
# Richten Sie die Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; nach einem der folgenden Abschnitte ein und speichern Sie.&lt;br /&gt;
# Starten Sie den REST-Dienst neu.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart http-01 ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Lassen Sie den Namen öffentlich auf die Adresse Ihres Internetanschlusses zeigen.&lt;br /&gt;
# Leiten Sie am Router Port 80 von aussen auf den Server des REST-Dienstes weiter.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; im Feld &#039;&#039;&#039;Prüfart&#039;&#039;&#039; den Eintrag &#039;&#039;http-01 (Port 80)&#039;&#039;. Die übrigen Felder bleiben leer.&lt;br /&gt;
# Geben Sie in der Konsole des REST-Servers &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Port 80 darf auf dem Server des REST-Dienstes von keinem anderen Dienst belegt sein, häufig ist das ein Webserver wie IIS. Der REST-Server öffnet den Port nur für die Dauer der Prüfung und schliesst ihn danach wieder.&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 direkt beim DNS-Anbieter ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Erstellen Sie beim Anbieter der Zone ein API-Token:&lt;br /&gt;
#* &#039;&#039;&#039;Hetzner:&#039;&#039;&#039; ein API-Token des Projekts mit Lese- und Schreibrecht in der Hetzner Console. Zonen in der früheren DNS-Konsole (dns.hetzner.com) werden nicht unterstützt.&lt;br /&gt;
#* &#039;&#039;&#039;Cloudflare:&#039;&#039;&#039; ein API-Token mit der Berechtigung, DNS zu bearbeiten, beschränkt auf die Zone.&lt;br /&gt;
#* &#039;&#039;&#039;IONOS:&#039;&#039;&#039; einen API-Schlüssel. Er besteht aus Präfix und Secret.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; im Feld &#039;&#039;&#039;Prüfart&#039;&#039;&#039; den Eintrag &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und im Feld &#039;&#039;&#039;DNS-Anbieter&#039;&#039;&#039; den Anbieter.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; das Token ein, bei IONOS in der Form &amp;lt;code&amp;gt;Präfix.Secret&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Lassen Sie &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; leer.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 mit CNAME auf eine Zone bei einem DNS-Anbieter ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Legen Sie in einer Zone bei Hetzner, Cloudflare oder IONOS einen Namen für die Prüfung fest, zum Beispiel &#039;&#039;obs-firma.acme.example.de&#039;&#039;. Einen Eintrag müssen Sie dort nicht anlegen.&lt;br /&gt;
# Legen Sie beim DNS-Anbieter Ihres Namens einen CNAME an: &amp;lt;code&amp;gt;_acme-challenge.obs.firma.de&amp;lt;/code&amp;gt; auf &#039;&#039;obs-firma.acme.example.de&#039;&#039;.&lt;br /&gt;
# Erstellen Sie ein API-Token für die Zone, in der das Ziel liegt, wie im vorigen Abschnitt beschrieben.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; die Prüfart &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und den Anbieter dieser Zone.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; das Token ein.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; das Ziel des CNAME ein, hier &#039;&#039;obs-firma.acme.example.de&#039;&#039;.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 mit CNAME auf acme-dns ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; die Prüfart &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und den DNS-Anbieter &#039;&#039;acme-dns&#039;&#039;.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Adresse (acme-dns)&#039;&#039;&#039; die Adresse der Instanz ein. Sie beginnt mit &amp;lt;code&amp;gt;https://&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Lassen Sie &#039;&#039;&#039;Benutzer (acme-dns)&#039;&#039;&#039;, &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; und &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; leer und speichern Sie.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein. Der Dienst legt das Konto bei acme-dns an und trägt Benutzer, Schlüssel und Name selbst ein. Unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; steht danach der CNAME, den Sie anlegen müssen.&lt;br /&gt;
# Legen Sie beim DNS-Anbieter Ihres Namens diesen CNAME an: &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf die genannte &#039;&#039;fulldomain&#039;&#039;.&lt;br /&gt;
# Geben Sie in der Konsole erneut &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Leeren Sie die drei Felder später nicht. Der Dienst würde ein neues Konto mit einer neuen &#039;&#039;fulldomain&#039;&#039; anlegen, und der gesetzte CNAME zeigte ins Leere. Ein bestehendes Konto tragen Sie mit allen drei Angaben ein. Teilweise gefüllte Angaben weist die Maske beim Speichern ab.}}&lt;br /&gt;
&lt;br /&gt;
=== Erstes Zertifikat holen ===&lt;br /&gt;
Ist der Selbsttest bestanden, holt der Dienst das Zertifikat bei der nächsten stündlichen Prüfung. Sofort geht es mit &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; in der Konsole. Bis das erste Zertifikat da ist, kann sich kein Client mit dem Server-Profil verbinden.&lt;br /&gt;
&lt;br /&gt;
=== Testbetrieb ===&lt;br /&gt;
{{Achtung|Im &#039;&#039;&#039;Testbetrieb&#039;&#039;&#039; beantragt der Dienst das Zertifikat beim Testsystem der Zertifizierungsstelle. Einem solchen Zertifikat vertraut &#039;&#039;&#039;kein&#039;&#039;&#039; Client, Anwendungen können sich danach nicht mehr mit diesem Server verbinden. Verwenden Sie den Testbetrieb ausschliesslich auf Testsystemen. Zurück in den Produktivbetrieb kommen Sie, indem Sie das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039; stellen. Der Dienst holt daraufhin sofort ein gültiges Zertifikat, auch wenn das Testzertifikat noch lange läuft.}}&lt;br /&gt;
&lt;br /&gt;
== Ergebnis ==&lt;br /&gt;
Nach bestandenem Selbsttest ist auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; der Haken &#039;&#039;&#039;Selbsttest bestanden&#039;&#039;&#039; gesetzt. Unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; steht &amp;quot;Selbsttest bestanden - von aussen erreichbar&amp;quot; (http-01) bzw. &amp;quot;Selbsttest bestanden - DNS-Eintrag von der CA geprueft&amp;quot; (dns-01). Nach der Ausstellung zeigt &#039;&#039;&#039;Zertifikat gültig bis&#039;&#039;&#039; das Ablaufdatum, und &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; in der Konsole zeigt den Stand je Profil.&lt;br /&gt;
&lt;br /&gt;
Danach läuft die Erneuerung selbständig:&lt;br /&gt;
# &#039;&#039;&#039;Prüfung:&#039;&#039;&#039; Der Dienst sieht stündlich nach, ob etwas ansteht.&lt;br /&gt;
# &#039;&#039;&#039;Selbsttest:&#039;&#039;&#039; Einmal wöchentlich und nach jeder Änderung von Zertifikatsname, Prüfart, DNS-Anbieter oder Name des TXT-Eintrags prüft der Dienst den Weg über das Testsystem der Zertifizierungsstelle. Besteht der Test nicht, beantragt er kein Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Erneuerung:&#039;&#039;&#039; Ab 30 Tagen Restlaufzeit beantragt der Dienst ein neues Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Übernahme:&#039;&#039;&#039; Der Dienst prüft das neue Zertifikat, speichert es und bewahrt das bisherige als Rückfall auf. Anschliessend startet nur das betroffene Server-Profil neu.&lt;br /&gt;
# &#039;&#039;&#039;Rückfall:&#039;&#039;&#039; Startet das Profil mit dem neuen Zertifikat nicht, aktiviert der Dienst wieder das vorherige.&lt;br /&gt;
&lt;br /&gt;
Nach einem Fehlschlag wartet der Dienst zunehmend länger, bevor er es erneut versucht: eine, zwei, vier, acht und höchstens 24 Stunden. Das Feld &#039;&#039;&#039;Gesperrt bis&#039;&#039;&#039; zeigt, bis wann. &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; in der Konsole wirken sofort, auch während der Sperre.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; || Zeigt je Profil Betriebsart, Ablaufdatum, Zustand und das Ergebnis des letzten Selbsttests&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; || Prüft sofort den Weg über das Testsystem der Zertifizierungsstelle. Beantragt kein Zertifikat&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; || Stösst die Erneuerung sofort an, auch wenn sie noch nicht fällig ist&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Bei dns-01 wartet der Dienst nach dem Schreiben des Eintrags 30 Sekunden und dann bis zu drei Minuten, bis der Eintrag über öffentliche DNS-Server sichtbar ist. Erst dann löst er die Prüfung aus.&lt;br /&gt;
&lt;br /&gt;
== Wenn es nicht klappt ==&lt;br /&gt;
Die Meldungen stehen unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; und im Protokoll des REST-Servers.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Meldung !! Ursache&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Port 80 laesst sich nicht oeffnen - er ist belegt&amp;quot; || Ein anderer Dienst auf dem Server des REST-Dienstes belegt Port 80, häufig IIS. Dienst abschalten oder die Prüfart dns-01 verwenden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;dns-01 ist eingestellt, aber kein DNS-Anbieter&amp;quot; || Auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; ist kein DNS-Anbieter gewählt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;kein API-Token&amp;quot; || Das Feld &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; ist leer&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;acme-dns: Konto unvollstaendig&amp;quot; || Von Benutzer, Schlüssel und Name des TXT-Eintrags ist nur ein Teil gefüllt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Hetzner: keine Zone zu … im Projekt des Tokens&amp;quot; || Das Token gehört zu einem anderen Projekt, oder die Zone liegt noch in der früheren DNS-Konsole&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Cloudflare: keine Zone zu …, die das Token sehen darf&amp;quot; || Das Token ist nicht für diese Zone freigegeben&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;IONOS: keine Zone zu … im Vertrag des Schluessels&amp;quot; || Der API-Schlüssel gehört zu einem anderen Vertrag&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;… lehnt den TXT-Eintrag ab (HTTP 401)&amp;quot; oder &amp;quot;(HTTP 403)&amp;quot; || Das Token ist ungültig oder darf keine DNS-Einträge ändern&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;… ist nach 210 Sekunden noch nicht ueberall sichtbar&amp;quot; || Der Eintrag ist über öffentliche DNS-Server nicht zu sehen. Meist fehlt der CNAME oder zeigt auf ein anderes Ziel als &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039;. Der Dienst löst die Prüfung trotzdem aus&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;kein oeffentlicher DNS-Server erreichbar&amp;quot; || Ausgehendes DNS (Port 53) ist gesperrt. Der Dienst kann die Sichtbarkeit nicht prüfen und wartet fest 60 Sekunden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;acme-dns-Konto angelegt - jetzt CNAME … setzen&amp;quot; || Kein Fehler: Legen Sie den genannten CNAME an und geben Sie &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Siehe auch ==&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile#Automatische Zertifikatserneuerung|Server-Profile: Felder der Zertifikatserneuerung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Scripting&amp;diff=67342</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Scripting&amp;diff=67342"/>
		<updated>2026-11-05T12:40:02Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Vorlage:-Praefix; Quelltext-Dateiname aus dem Hinweis entfernt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Anleitung für Endpunkt-Skripte=&lt;br /&gt;
&lt;br /&gt;
Jede Anfrage an einen Endpunkt wird nach erfolgreicher Authentifizierung und Autorisierung an das hinterlegte Skript weitergegeben. Das Skript ist in Object Pascal geschrieben und greift auf die OBS-Bibliothek (DB-Zugriff, Hilfsfunktionen) zu.&lt;br /&gt;
&lt;br /&gt;
==Sicherheitshinweise==&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Endpunkt-Skripte verarbeiten Daten aus dem Internet. Übergabeparameter dürfen niemals ungeprüft in SQL-Anweisungen eingebaut werden. Werte immer über &#039;&#039;DB_SQLVal&#039;&#039; bzw. Parameter-Bindings absichern, Eingabewerte gegen Whitelists prüfen.}}&lt;br /&gt;
&lt;br /&gt;
* Eingaben gegen erwartete Werte prüfen (z.B. Pflichtfelder, erlaubte Typen, Wertebereiche).&lt;br /&gt;
* Nur das zurückgeben, was der Konsument wirklich braucht - keine internen IDs, keine Sys-Felder, keine Passwörter.&lt;br /&gt;
* Bei Fehlern keine internen Details an den Konsumenten zurückgeben; stattdessen schreibt der Server ohnehin Detail-Einträge in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==Methoden-Signatur==&lt;br /&gt;
&lt;br /&gt;
Pro HTTP-Methode wird im Skript eine gleichnamige &#039;&#039;&#039;Prozedur&#039;&#039;&#039; implementiert. Der Server ruft genau die Prozedur auf, die zur Methode des eingehenden Requests passt.&lt;br /&gt;
&lt;br /&gt;
 procedure Get   (oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
 procedure Post  (oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
 procedure Put   (oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
 procedure Patch (oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
&lt;br /&gt;
Zwei Handles, kein Rückgabewert: &#039;&#039;&#039;oReader&#039;&#039;&#039; ist die Anfrage, &#039;&#039;&#039;oWriter&#039;&#039;&#039; die&lt;br /&gt;
Antwort. Beide erzeugt der Server, beide gehören ihm - im Skript wird nichts&lt;br /&gt;
angelegt und nichts freigegeben.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;DELETE hat keine Skript-Methode.&#039;&#039;&#039; &amp;lt;code&amp;gt;Delete&amp;lt;/code&amp;gt; ist in der&lt;br /&gt;
Skriptsprache eine Standardprozedur (Löschen aus einer Zeichenkette); eine eigene&lt;br /&gt;
Prozedur dieses Namens lässt sich nicht übersetzen, und der Fehler nennt nur die&lt;br /&gt;
Zeile der Deklaration. Wer einen DELETE-Endpunkt braucht, legt die Methode unter&lt;br /&gt;
einem anderen Verb ab oder trennt sie in ein eigenes Skript.}}&lt;br /&gt;
&lt;br /&gt;
Fehlt die zur Methode passende Funktion, antwortet der Server mit &#039;&#039;&#039;405 Method&lt;br /&gt;
Not Allowed&#039;&#039;&#039; (&amp;lt;code&amp;gt;METHOD_NOT_ALLOWED&amp;lt;/code&amp;gt;) und nennt im Header&lt;br /&gt;
&amp;lt;code&amp;gt;Allow&amp;lt;/code&amp;gt; die Verben, die dieses Skript tatsächlich anbietet - die Liste&lt;br /&gt;
stammt aus dem kompilierten Skript und kann deshalb nicht veralten:&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 405 Method Not Allowed&lt;br /&gt;
 Allow: GET, PUT&lt;br /&gt;
&lt;br /&gt;
Ein im Vertrag zugesagtes Verb braucht also seine Prozedur. Fehlt sie, ist das an&lt;br /&gt;
der Antwort sofort zu erkennen und &#039;&#039;&#039;kein&#039;&#039;&#039; Serverfehler.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Die Anfrage lesen: oReader==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;oReader&#039;&#039; ist der einzige Zugang zur Anfrage - Query, Header, Formularfelder,&lt;br /&gt;
Pfadwerte, Token-Claims und Körper. Alles davon sind &#039;&#039;&#039;Zeichenketten&#039;&#039;&#039; oder&lt;br /&gt;
werden über einen typisierten Leser geholt.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Was er liefert&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;kundennr&#039;)&amp;lt;/code&amp;gt; || Query-Parameter, POST-Parameter (form-urlencoded) oder HTTP-Header. Getrimmt, leer wenn nicht gesendet&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Path(&#039;uid&#039;)&amp;lt;/code&amp;gt; || Wert eines Platzhalters aus dem Pfad-Template&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Form(&#039;bemerkung&#039;)&amp;lt;/code&amp;gt; || Formularfeld einer &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt;-Übertragung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Claim(&#039;tenant&#039;)&amp;lt;/code&amp;gt; || Custom-Claim des vorgelegten Tokens&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Subject()&amp;lt;/code&amp;gt; || Subject des Tokens (&#039;&#039;sub&#039;&#039;-Claim)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.TraceId()&amp;lt;/code&amp;gt; || Korrelations-ID des Requests&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Offset()&amp;lt;/code&amp;gt; || UTC-Offset des Servers, z.B. &#039;&#039;+02:00&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.HasBody()&amp;lt;/code&amp;gt; || Wurde überhaupt ein JSON-Körper gesendet?&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Filterung durch den Server:&lt;br /&gt;
&lt;br /&gt;
* Geblockte Header werden nicht durchgereicht: &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, &#039;&#039;proxy-authorization&#039;&#039;, &#039;&#039;x-forwarded-for&#039;&#039;, &#039;&#039;x-real-ip&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;api_key&#039;&#039;.&lt;br /&gt;
* Werte, die von aussen kommen, können die reservierten Namen nicht besetzen.&lt;br /&gt;
* Werte werden auf max. 1024 Zeichen begrenzt.&lt;br /&gt;
* Null-Bytes und Steuerzeichen (ausser Tab, CR, LF) werden entfernt.&lt;br /&gt;
&lt;br /&gt;
Zugriff im Skript:&lt;br /&gt;
&lt;br /&gt;
 cKundenNr := oReader.Param(&#039;kundennr&#039;);&lt;br /&gt;
 if (not Empty(cKundenNr)) then begin&lt;br /&gt;
     // ...&lt;br /&gt;
 end;&lt;br /&gt;
&lt;br /&gt;
===Der Körper: Zugriff über Pfade===&lt;br /&gt;
&lt;br /&gt;
Den JSON-Körper liest &#039;&#039;oReader&#039;&#039; über &#039;&#039;&#039;Pfade&#039;&#039;&#039;. Es gibt keine Unterobjekte im&lt;br /&gt;
Skript, keinen Cast, kein &#039;&#039;.Items[i]&#039;&#039; - und damit auch keine Lebensdauer, um die&lt;br /&gt;
sich jemand kümmern müsste.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Str(&#039;betreff&#039;, cWert)&amp;lt;/code&amp;gt; || Liest den Wert. &#039;&#039;&#039;Rückgabe Boolean&#039;&#039;&#039;: war das Feld da? Fehlt es, bleibt die Variable unverändert&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Int(&#039;menge&#039;, nWert)&amp;lt;/code&amp;gt; || dito, Ganzzahl&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Num(&#039;preis&#039;, nWert)&amp;lt;/code&amp;gt; || dito, Kommazahl. &#039;&#039;&#039;Der Dezimalpunkt aus JSON wird richtig gelesen&#039;&#039;&#039; - kein &#039;&#039;StrTran&#039;&#039; mehr&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Bool(&#039;aktiv&#039;, lWert)&amp;lt;/code&amp;gt; || dito, Boolean&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.NumDef(&#039;rabatt&#039;, nWert, 0)&amp;lt;/code&amp;gt; || wie &#039;&#039;Num&#039;&#039;, mit Vorgabewert&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Has(&#039;feld&#039;)&amp;lt;/code&amp;gt; || Ist das Feld vorhanden? (auch bei &#039;&#039;null&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Count(&#039;zeiten&#039;)&amp;lt;/code&amp;gt; || Anzahl Einträge einer Liste, 0 wenn keine Liste&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.IsArr(&#039;zeiten&#039;)&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;IsObj(&#039;zeiten[0]&#039;)&amp;lt;/code&amp;gt; || Gestalt prüfen&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Keys()&amp;lt;/code&amp;gt; || Alle Feldnamen der obersten Ebene, in Kommas eingefasst&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Verschachtelung steht im Pfad&#039;&#039;&#039; - Punkt für Felder, eckige Klammern für Listen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Put(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cVon : string;&lt;br /&gt;
    nAnz : Integer;&lt;br /&gt;
    nI   : Integer;&lt;br /&gt;
begin&lt;br /&gt;
    nAnz := oReader.Count(&#039;zeiten&#039;);&lt;br /&gt;
    for nI := 0 to nAnz - 1 do begin&lt;br /&gt;
        if (oReader.Str(&#039;zeiten[&#039; + IntToStr(nI) + &#039;].von&#039;, cVon)) then begin&lt;br /&gt;
            // ... cVon verarbeiten ...&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Der Rückgabewert unterscheidet „nicht gesendet&amp;quot; von „leer&lt;br /&gt;
gesendet&amp;quot;.&#039;&#039;&#039; &amp;lt;code&amp;gt;oReader.Str(&#039;feld&#039;, cWert)&amp;lt;/code&amp;gt; liefert bei&lt;br /&gt;
&amp;lt;code&amp;gt;&amp;quot;feld&amp;quot;: null&amp;lt;/code&amp;gt; &#039;&#039;true&#039;&#039; mit leerem Wert und bei fehlendem Feld&lt;br /&gt;
&#039;&#039;false&#039;&#039;. Genau diese Unterscheidung braucht ein PATCH-artiger Aufruf: „Feld&lt;br /&gt;
löschen&amp;quot; und „Feld nicht anfassen&amp;quot; sehen sonst gleich aus.}}&lt;br /&gt;
&lt;br /&gt;
Ist gar kein Körper gesendet worden, liefert jeder Leser &#039;&#039;false&#039;&#039; und&lt;br /&gt;
&#039;&#039;HasBody()&#039;&#039; ist &#039;&#039;false&#039;&#039;. Maximale Body-Grösse: 10 MB - bei einem gepackt gesendeten Körper zusätzlich das 400fache der gesendeten Grösse, siehe [[OBS/Kostenpflichtige Module/RESTServer|REST-Server]], Abschnitt Kompression. Beide Überschreitungen beantwortet der Server mit &#039;&#039;&#039;413&#039;&#039;&#039;, das Skript läuft gar nicht erst an.&lt;br /&gt;
&lt;br /&gt;
===Werte des Servers===&lt;br /&gt;
&lt;br /&gt;
Bei aktiver JWT-Authentifizierung stehen die Token-Claims über den Reader zur&lt;br /&gt;
Verfügung. Sie tragen intern weiterhin die Namen mit Präfix &#039;&#039;&#039;_OBS_&#039;&#039;&#039; - im&lt;br /&gt;
Skript werden sie aber über die Zugriffe der Tabelle oben gelesen und nicht mehr&lt;br /&gt;
über den Namen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Parameter            !! Inhalt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;_OBS_JWT_ID&#039;)&amp;lt;/code&amp;gt; || JWT-Id (&#039;&#039;jti&#039;&#039;-Claim) - typisch die User-Id. &#039;&#039;&#039;Muss nicht eindeutig sein&#039;&#039;&#039;: die Sitzung führt der Server über eigene Kennungen (siehe unten)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Subject()&amp;lt;/code&amp;gt; || Subject (&#039;&#039;sub&#039;&#039;-Claim) - typisch Benutzername&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;_OBS_JWT_AUDIENCE&#039;)&amp;lt;/code&amp;gt; || Audience (&#039;&#039;aud&#039;&#039;-Claim) - typisch Mandant / Rolle&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Claim(&#039;tenant&#039;)&amp;lt;/code&amp;gt; || Beliebiger Custom-Claim des Tokens, hier &#039;&#039;tenant&#039;&#039;; im Folge-Skript lesbar&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.TraceId()&amp;lt;/code&amp;gt; || Korrelations-ID des Requests (auch als Response-Header X-Trace-Id)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Unabhängig von JWT stehen ausserdem immer zur Verfügung:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Parameter            !! Inhalt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;_OBS_SERVER_TIME&#039;)&amp;lt;/code&amp;gt; || Aktuelle Serverzeit als ISO-8601 &#039;&#039;&#039;mit UTC-Offset&#039;&#039;&#039; (z.B. &#039;&#039;2026-08-17T09:12:33+02:00&#039;&#039;). Nützlich, um Datumswerte in derselben Zeitzone auszuliefern, in der der Server arbeitet, und damit ein Client seinen Uhren-Versatz bestimmen kann&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Offset()&amp;lt;/code&amp;gt; || Nur der Offset daraus, z.B. &#039;&#039;+02:00&#039;&#039;. Er geht in die Zeitfunktionen (siehe [[#Zeitangaben auf der Leitung|Zeitangaben]])&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;_OBS_JWT_EXP_SEC&#039;)&amp;lt;/code&amp;gt; || Laufzeit des Access-Tokens in Sekunden (&#039;&#039;JWT-Exp&#039;&#039; × 60). Damit kann ein Konfigurations-Endpunkt denselben Wert ausliefern, den der Token wirklich hat&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Diese Werte werden vom Server gesetzt und können vom Skript für Berechtigungs- und Mandantenprüfungen verwendet werden.&lt;br /&gt;
&lt;br /&gt;
Zusätzlich trägt jedes Token zwei &#039;&#039;&#039;Sitzungskennungen des Servers&#039;&#039;&#039;, lesbar als&lt;br /&gt;
&#039;&#039;oReader.Claim(&#039;sid&#039;)&#039;&#039; und &#039;&#039;oReader.Claim(&#039;rid&#039;)&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Claim !! Inhalt&lt;br /&gt;
|-&lt;br /&gt;
| sid || Sitzung. Bleibt über alle Erneuerungen (Refresh) hinweg gleich und ist der Schlüssel für das Abmelden&lt;br /&gt;
|-&lt;br /&gt;
| rid || Zeilenkennung des einzelnen Token-Paares in &#039;&#039;RESTSRV_TOKEN&#039;&#039;. Bei jeder Erneuerung neu&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beide sind OBS-UIDs (10 Zeichen) und werden vom Server &#039;&#039;&#039;nach&#039;&#039;&#039; den Claims des&lt;br /&gt;
Skripts gesetzt - ein Skript kann sie also auch mit einem eigenen Claim &#039;&#039;sid&#039;&#039;&lt;br /&gt;
nicht überschreiben. Für Fachlogik sind sie nicht gedacht; sie sind nützlich, um eine&lt;br /&gt;
Sitzung im Protokoll wiederzufinden.&lt;br /&gt;
&lt;br /&gt;
===Pfad-Parameter===&lt;br /&gt;
&lt;br /&gt;
Stammt der Endpunkt aus einem Pfad-Template mit Platzhaltern (siehe [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]), stehen die aus den Platzhaltern erfassten Werte über &#039;&#039;oReader.Path&#039;&#039; bereit:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Template !! Zugriff im Skript&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;oReader.Path(&#039;uid&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders/{uid}/modules/{code}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;oReader.Path(&#039;uid&#039;)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;oReader.Path(&#039;code&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Diese Werte kommen aus dem Routing und sind durch den Client nicht fälschbar. Ein vollständiges Beispiel zeigt [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4 - Pfad-Parameter]].&lt;br /&gt;
&lt;br /&gt;
===Datei-Uploads===&lt;br /&gt;
&lt;br /&gt;
Ist der Endpunkt für Uploads freigeschaltet (&amp;lt;code&amp;gt;re_upload = 1&amp;lt;/code&amp;gt;, siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]), nimmt der Server&lt;br /&gt;
hochgeladene Dateien entgegen, legt sie in einem temporären Verzeichnis ab und&lt;br /&gt;
übergibt dem Skript Pfad, Originalname und Content-Type über den Reader. Das&lt;br /&gt;
Skript entscheidet selbst über die weitere Verarbeitung (z.B. DMS-Ablage). Der&lt;br /&gt;
Body wird in diesem Fall &#039;&#039;&#039;nicht&#039;&#039;&#039; als JSON geparst - &#039;&#039;oReader.HasBody()&#039;&#039; ist&lt;br /&gt;
&#039;&#039;false&#039;&#039;. Es gilt nicht das JSON-Body-Limit (10&amp;amp;nbsp;MB), sondern die pro&lt;br /&gt;
Endpunkt konfigurierte Grösse (&#039;&#039;re_upload_size&#039;&#039;, Standard 25&amp;amp;nbsp;MB;&lt;br /&gt;
Überschreitung -&amp;gt; 413).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Wie die Datei übertragen wurde, spielt für das Skript keine Rolle.&#039;&#039;&#039; Beide&lt;br /&gt;
Wege - &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; und der resumable&lt;br /&gt;
&amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;-Upload - liefern dieselben vier Werte. Das&lt;br /&gt;
Übertragungsprotokoll samt Header, Statuscodes und Resume-Verhalten steht in&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]]:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Inhalt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadPath()&amp;lt;/code&amp;gt; || Vollständiger Pfad zur temporären Datei auf dem Server&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadName()&amp;lt;/code&amp;gt; || Originaldateiname&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadType()&amp;lt;/code&amp;gt; || Content-Type der Datei (Default &amp;lt;code&amp;gt;application/octet-stream&amp;lt;/code&amp;gt;, wenn der Client keinen angibt), kleingeschrieben&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadSha()&amp;lt;/code&amp;gt; || SHA-256 der &#039;&#039;&#039;gespeicherten&#039;&#039;&#039; Datei, hex in Kleinbuchstaben. Damit lässt sich eine vom Client mitgesendete Prüfsumme vergleichen - erst dieser Vergleich macht aus ihr eine Zusicherung&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Der &#039;&#039;&#039;Dateiname ist Pflicht&#039;&#039;&#039;. Fehlt er, lehnt der Server den Upload mit&lt;br /&gt;
&#039;&#039;&#039;400 Bad Request&#039;&#039;&#039; ab und das Skript wird nicht ausgeführt - das Skript kann&lt;br /&gt;
sich also darauf verlassen, dass &#039;&#039;UploadPath()&#039;&#039; und &#039;&#039;UploadName()&#039;&#039; gefüllt&lt;br /&gt;
sind, sobald es läuft.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Die temporäre Datei wird nicht automatisch verschoben. Das Skript muss sie an ihren Zielort (DMS, Verzeichnis, ...) übernehmen.}}&lt;br /&gt;
&lt;br /&gt;
Zusatzangaben, die im selben Aufruf mitkommen, liest das Skript mit&lt;br /&gt;
&amp;lt;code&amp;gt;oReader.Form(&#039;&amp;amp;lt;name&amp;amp;gt;&#039;)&amp;lt;/code&amp;gt; - bei &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; sind&lt;br /&gt;
das die Partien ohne &amp;lt;code&amp;gt;filename=&amp;lt;/code&amp;gt;. Ein vollständiges Beispiel zeigt&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6]].&lt;br /&gt;
&lt;br /&gt;
==Die Antwort schreiben: oWriter==&lt;br /&gt;
&lt;br /&gt;
Der Writer schreibt &#039;&#039;&#039;sequentiell&#039;&#039;&#039;: jeder Aufruf hängt ein Feld an, an der&lt;br /&gt;
Stelle, an der er steht. Die äussere Klammer der Antwort setzt der Server. Wird&lt;br /&gt;
nichts geschrieben, antwortet er mit &#039;&#039;{}&#039;&#039;. Content-Type ist&lt;br /&gt;
&#039;&#039;application/json; charset=utf-8&#039;&#039;, der Status 200, sofern das Skript nichts&lt;br /&gt;
anderes setzt.&lt;br /&gt;
&lt;br /&gt;
Einfaches Beispiel:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    oWriter.Str(&#039;wert_string&#039;, &#039;123&#039;);&lt;br /&gt;
    oWriter.Int(&#039;wert_int&#039;   , 456);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Ergebnis&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Str(&#039;name&#039;, cWert)&amp;lt;/code&amp;gt; || Zeichenkette, immer maskiert&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.StrN(&#039;name&#039;, cWert)&amp;lt;/code&amp;gt; || wie &#039;&#039;Str&#039;&#039;, aber &#039;&#039;&#039;null&#039;&#039;&#039; statt einer leeren Zeichenkette&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Int(&#039;anz&#039;, nWert)&amp;lt;/code&amp;gt; || Ganzzahl&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Num(&#039;preis&#039;, nWert, 2)&amp;lt;/code&amp;gt; || Kommazahl mit fester Stellenzahl. &#039;&#039;&#039;Immer mit Dezimalpunkt&#039;&#039;&#039;, unabhängig von der Locale des Servers&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Bool(&#039;aktiv&#039;, lWert)&amp;lt;/code&amp;gt; || &#039;&#039;true&#039;&#039; / &#039;&#039;false&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Null(&#039;lat&#039;)&amp;lt;/code&amp;gt; || ausdrücklich &#039;&#039;null&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.ObjBegin(&#039;kunde&#039;)&amp;lt;/code&amp;gt; … &amp;lt;code&amp;gt;oWriter.ObjEnd&amp;lt;/code&amp;gt; || Unterobjekt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.ArrBegin(&#039;dateien&#039;)&amp;lt;/code&amp;gt; … &amp;lt;code&amp;gt;oWriter.ArrEnd&amp;lt;/code&amp;gt; || Liste&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.RootArr()&amp;lt;/code&amp;gt; || Die &#039;&#039;&#039;Wurzel&#039;&#039;&#039; ist eine Liste statt eines Objekts. Muss vor dem ersten Feld stehen; ein &#039;&#039;ArrEnd&#039;&#039; gibt es dazu nicht&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Raw(&#039;meta&#039;, cJson&#039;)&amp;lt;/code&amp;gt; || Fertiges JSON-Fragment einbetten. Siehe unten&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;In einer Liste hat ein Eintrag keinen Namen&#039;&#039;&#039; - der leere Feldname ist genau&lt;br /&gt;
das:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
oWriter.ArrBegin(&#039;dateien&#039;);&lt;br /&gt;
    while (not q.EoF) do begin&lt;br /&gt;
        oWriter.ObjBegin(&#039;&#039;);&lt;br /&gt;
            oWriter.Str (&#039;uid&#039;      , q.A2C(&#039;f_uid&#039;));&lt;br /&gt;
            oWriter.Int (&#039;bytes&#039;    , q.A2I(&#039;f_bytes&#039;));&lt;br /&gt;
            oWriter.StrN(&#039;kategorie&#039;, AllTrim(q.A2C(&#039;f_kat&#039;)));   // null wenn leer&lt;br /&gt;
        oWriter.ObjEnd;&lt;br /&gt;
        q.Next();&lt;br /&gt;
    end;&lt;br /&gt;
oWriter.ArrEnd;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ein Name in einer Liste und ein fehlender Name in einem Objekt sind beides&lt;br /&gt;
&#039;&#039;&#039;Strukturfehler&#039;&#039;&#039;. Der Server antwortet dann mit &#039;&#039;&#039;500&#039;&#039;&#039; und nennt die&lt;br /&gt;
Stelle im Protokoll - er liefert kein halbes JSON aus. Dasselbe gilt für einen&lt;br /&gt;
Stapel, der am Ende der Methode noch offen ist.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Ein Helfer bekommt den Writer als Parameter&#039;&#039;&#039; und schreibt an der&lt;br /&gt;
Stelle hinein, an der er aufgerufen wird - er gibt kein Fragment zurück. Damit&lt;br /&gt;
steht die Reihenfolge der Antwort im Code und nicht im Zusammenbau:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;procedure DateienSchreiben(oWriter: TxRestWriter; const cNr: string);&amp;lt;/code&amp;gt;}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Raw ist der Notausgang und prüft.&#039;&#039;&#039; Er parst das Fragment und bettet den&lt;br /&gt;
&#039;&#039;&#039;Originaltext&#039;&#039;&#039; ein; ungültiges JSON gibt einen Serverfehler, der das Feld&lt;br /&gt;
nennt. Gebraucht wird er für ein gespeichertes, &#039;&#039;&#039;signiertes&#039;&#039;&#039; Dokument:&lt;br /&gt;
Parsen und Neuserialisieren würde die signierte Feldreihenfolge zerstören.&lt;br /&gt;
&lt;br /&gt;
===Zeitangaben auf der Leitung===&lt;br /&gt;
&lt;br /&gt;
Zeitstempel gehen als &#039;&#039;&#039;ISO-8601 mit Offset&#039;&#039;&#039; über die Leitung. Die Wandlung&lt;br /&gt;
gehört nicht ins Skript - dort saß der Fehler „zwei Stunden zu früh&amp;quot; an drei&lt;br /&gt;
Stellen gleichzeitig:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Zweck&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;RestIsoFromDate(dWert, cOffset)&amp;lt;/code&amp;gt; || OBS-Zeitpunkt -&amp;gt; &#039;&#039;2026-09-09T14:12:00+02:00&#039;&#039;. Leerer String bei leerem Datum&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;RestDateFromIso(cWert, cOffset)&amp;lt;/code&amp;gt; || ISO-Zeichenkette -&amp;gt; OBS-Zeitpunkt in &#039;&#039;&#039;Ortszeit des Servers&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Offset()&amp;lt;/code&amp;gt; || Der Offset, der in beide hineingeht&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;RestOffsetToMinutes(cOffset)&amp;lt;/code&amp;gt; || Offset in Minuten, für eigene Rechnungen&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Antwort steuern: Statuscode, Header, traceId==&lt;br /&gt;
&lt;br /&gt;
Statuscode, Header und das Sperren einer Sitzung laufen über eigene Aufrufe des&lt;br /&gt;
Writers, &#039;&#039;&#039;nicht&#039;&#039;&#039; über Felder im Körper.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Status(201)&amp;lt;/code&amp;gt; || HTTP-Statuscode (z.B. 201, 204, 400, 409, 422). Ohne Angabe: 200&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Header(&#039;ETag&#039;, &#039;7&#039;)&amp;lt;/code&amp;gt; || Beliebiger Response-Header, z.B. ETag, Location, Retry-After. CR/LF in Namen und Werten werden entfernt (Schutz vor Header-Injection)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.NoBody()&amp;lt;/code&amp;gt; || Kein Körper - für &#039;&#039;&#039;204&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.JwtRevoke(&#039;session&#039;)&amp;lt;/code&amp;gt; || Sperrt die Sitzung des vorgelegten Tokens (&#039;&#039;session&#039;&#039;) oder alle Sitzungen des Subjects (&#039;&#039;all&#039;&#039;). Damit wird ein Endpunkt zum Logout, ohne eigene Verwaltung. Scheitert das Sperren, antwortet der Server mit &#039;&#039;&#039;503&#039;&#039;&#039; statt mit der Antwort des Skripts - siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]], Abschnitt Abmelden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Feld „menge&amp;quot; fehlt&#039;)&amp;lt;/code&amp;gt; || Fertige Fehlerantwort in der Form des Servers, siehe [[#Fehlerbehandlung im Skript|Fehlerbehandlung]]&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.HasError()&amp;lt;/code&amp;gt; || Steht schon eine Fehlerantwort im Writer? Für einen Verteiler, der danach noch etwas anhängen will&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.SendFile(cPfad, cName, cTyp)&amp;lt;/code&amp;gt; || Statt eines JSON-Körpers eine &#039;&#039;&#039;Datei&#039;&#039;&#039; ausliefern. Die Datei bleibt liegen, der Download ist wiederaufsetzbar. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]]&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.SendTempFile(cPfad, cName, cTyp)&amp;lt;/code&amp;gt; || Wie &amp;lt;code&amp;gt;SendFile&amp;lt;/code&amp;gt;, aber die Datei wird nach dem Senden &#039;&#039;&#039;gelöscht&#039;&#039;&#039; und der Download ist nicht wiederaufsetzbar - für Ergebnisse, die je Abruf entstehen (CSV-Export, Report)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Zusätzlich trägt jede Antwort den Header &#039;&#039;&#039;X-Trace-Id&#039;&#039;&#039; (Korrelations-ID). Dieselbe ID liegt dem Skript als &#039;&#039;oReader.TraceId()&#039;&#039; vor und erscheint in jeder Server-Fehler-Logzeile in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; - so lässt sich ein Fehler ohne Gerätezugriff im Log wiederfinden.&lt;br /&gt;
&lt;br /&gt;
Setzt ein Skript den Header &#039;&#039;&#039;Content-Encoding&#039;&#039;&#039; selbst - etwa weil es einen bereits gepackten Körper ausliefert -, hält sich die Engine heraus und packt die Antwort nicht noch einmal. Ohne diesen Header entscheidet der Server selbst, ob gzip zum Einsatz kommt.&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Anlegen mit 201, Location und ETag===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cNeueUid: string;&lt;br /&gt;
begin&lt;br /&gt;
    // ... Datensatz anlegen, neue UID + Version (ETag) ermitteln ...&lt;br /&gt;
&lt;br /&gt;
    oWriter.Str(&#039;uid&#039;, cNeueUid);&lt;br /&gt;
    oWriter.Status(201);&lt;br /&gt;
    oWriter.Header(&#039;Location&#039;, &#039;/v1/orders/&#039; + cNeueUid);&lt;br /&gt;
    oWriter.Header(&#039;ETag&#039;    , &#039;1&#039;);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Strukturierter Fehler mit Statuscode und traceId===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var nMenge: Integer;&lt;br /&gt;
begin&lt;br /&gt;
    if (not oReader.Int(&#039;menge&#039;, nMenge)) then begin&lt;br /&gt;
        // Statuscode, code, message, uid und traceId in EINEM Aufruf - dieselbe&lt;br /&gt;
        // Huelle, die auch die Server-eigenen Fehler tragen.&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Feld &amp;quot;menge&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
    // ...&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Optimistic Concurrency (ETag / If-Match)===&lt;br /&gt;
&lt;br /&gt;
Mit Statuscode, Headern und dem lesbaren Header &#039;&#039;If-Match&#039;&#039; führt das Skript je Datensatz einen Versionszähler und lehnt veraltete Schreibzugriffe mit 409 ab:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Put(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var nAktuell: Integer;&lt;br /&gt;
    nIfMatch: Integer;&lt;br /&gt;
begin&lt;br /&gt;
    nAktuell := AuftragVersion(oReader.Path(&#039;uid&#039;));&lt;br /&gt;
    nIfMatch := iVal(oReader.Param(&#039;if-match&#039;));&lt;br /&gt;
&lt;br /&gt;
    if (nIfMatch &amp;lt;&amp;gt; nAktuell) then begin&lt;br /&gt;
        // Der ETag gehoert AUCH an die Ablehnung - sonst kennt der Client den&lt;br /&gt;
        // gueltigen Wert nicht und sein naechster Versuch scheitert wieder.&lt;br /&gt;
        oWriter.Header(&#039;ETag&#039;, xStr(nAktuell));&lt;br /&gt;
        oWriter.Error(409, &#039;VERSION_CONFLICT&#039;, &#039;Der Datensatz wurde zwischenzeitlich geaendert&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // ... speichern, Version hochzählen ...&lt;br /&gt;
    oWriter.Header(&#039;ETag&#039;, xStr(nAktuell + 1));&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Idempotenz - was das Skript beachten muss==&lt;br /&gt;
&lt;br /&gt;
Bei &#039;&#039;POST&#039;&#039;/&#039;&#039;PUT&#039;&#039;/&#039;&#039;PATCH&#039;&#039;/&#039;&#039;DELETE&#039;&#039; fängt der &#039;&#039;&#039;Server&#039;&#039;&#039; doppelte&lt;br /&gt;
Sendungen über den Header &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; ab. Das Skript braucht dafür&lt;br /&gt;
&#039;&#039;&#039;keine eigene Logik&#039;&#039;&#039; - keine Schlüssel-Tabelle, keine Prüfung am Anfang der&lt;br /&gt;
Methode.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Header ist bei diesen vier Verben &#039;&#039;&#039;Pflicht&#039;&#039;&#039;. Fehlt er, antwortet&lt;br /&gt;
der Server mit &#039;&#039;&#039;400&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt; - das Skript läuft&lt;br /&gt;
dann gar nicht erst an, es muss den Fall also auch nicht behandeln.}}&lt;br /&gt;
&lt;br /&gt;
Was das Skript wissen muss:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Antwort des Skripts !! Wirkung auf den Idempotenz-Speicher&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;2xx&#039;&#039;&#039; || Statuscode, Body und Header werden gespeichert. Eine Wiederholung mit demselben Schlüssel bekommt genau diese Antwort zurück, das Skript läuft nicht erneut&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;2xx&#039;&#039;&#039; mit &#039;&#039;SendFile&#039;&#039;/&#039;&#039;SendTempFile&#039;&#039; || &#039;&#039;&#039;Nichts&#039;&#039;&#039; wird gespeichert - eine Dateiantwort liesse sich nicht wiedergeben. Der Schlüssel wird freigegeben und ist danach wieder benutzbar&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;4xx&#039;&#039;&#039; || Der Schlüssel wird &#039;&#039;&#039;freigegeben&#039;&#039;&#039;. Der Konsument darf denselben Schlüssel nach Korrektur des Inhalts erneut verwenden&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;5xx&#039;&#039;&#039; oder Exception || Der Schlüssel wird ebenfalls &#039;&#039;&#039;freigegeben&#039;&#039;&#039; und der Fall protokolliert. Der Konsument darf denselben Schlüssel erneut senden&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Das 5xx-Verhalten wurde am 25.08.2026 gedreht.&#039;&#039;&#039; Vorher blieb der&lt;br /&gt;
Schlüssel nach einem Serverfehler belegt - begründet mit „niemals eine&lt;br /&gt;
Doppelbuchung&amp;quot;. Das war falsch gewichtet: der Vorgang wurde damit&lt;br /&gt;
&#039;&#039;&#039;unwiederholbar&#039;&#039;&#039;, der Client bekam dauerhaft 503 und hätte die erfasste Arbeit&lt;br /&gt;
verwerfen müssen. Garantierter Datenverlust wiegt schwerer als ein möglicher&lt;br /&gt;
Doppelsatz.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Folge für die Auslegung:&#039;&#039;&#039; Ein schreibender Endpunkt sollte selbst&lt;br /&gt;
wiederholbar sein, etwa über ein fachliches Merkmal des Clients. Wo reines&lt;br /&gt;
Anhängen stattfindet (Positionen, Dateien), bleibt sonst ein Restrisiko.}}&lt;br /&gt;
&lt;br /&gt;
Daraus folgen zwei Regeln für Endpunkt-Skripte:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Fachliche Ablehnungen als 4xx melden&#039;&#039;&#039; (422, 409, 404), nicht als 2xx mit Fehlertext im Body. Sonst wird die Ablehnung gespeichert und der Konsument bekommt sie bei jedem weiteren Versuch erneut - auch nachdem er den Fehler behoben hat.&lt;br /&gt;
* &#039;&#039;&#039;Die Antwort muss vollständig sein.&#039;&#039;&#039; Was zurückgegeben wird, wird eingefroren. Ein Feld, das erst der zweite Aufruf ergänzen würde, kommt beim Konsumenten nie an.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Der vollständige Ablauf und die Statuscodes stehen unter&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].&lt;br /&gt;
&lt;br /&gt;
==JWT-Authentifizierungs-Skript==&lt;br /&gt;
&lt;br /&gt;
Bei Zugängen mit aktiver JWT-Pflicht wird über den JWT-Endpunkt das im Zugang hinterlegte Authentifizierungs-Skript aufgerufen (F7 in der Zugänge-Liste). Das Skript muss eine Methode &#039;&#039;Authenticate&#039;&#039; bereitstellen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Authenticate(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUser: string;&lt;br /&gt;
    cPass: string;&lt;br /&gt;
begin&lt;br /&gt;
    // Eingangsdaten lesen&lt;br /&gt;
    cUser := &#039;&#039;;&lt;br /&gt;
    cPass := &#039;&#039;;&lt;br /&gt;
    oReader.Str(&#039;username&#039;, cUser);&lt;br /&gt;
    oReader.Str(&#039;password&#039;, cPass);&lt;br /&gt;
&lt;br /&gt;
    // Prüfung gegen eigene Tabelle, Hash-Verfahren, LDAP, ...&lt;br /&gt;
    if (PasswortPasst(cUser, cPass)) then begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;           , 1);&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_ID&#039;      , cUser);&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_SUBJECT&#039; , cUser);&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_AUDIENCE&#039;, &#039;mandant1&#039;);&lt;br /&gt;
    end else begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;, 9);&lt;br /&gt;
        oWriter.Str(&#039;error&#039; , &#039;Login fehlgeschlagen&#039;);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Im Anmeldeskript bleiben die _OBS_JWT_*-Felder Felder der&lt;br /&gt;
Antwort&#039;&#039;&#039; - anders als Status und Header, die zu Aufrufen des Writers geworden&lt;br /&gt;
sind. Der Grund: die Token-Ausstellung liest sie aus dem fertigen Körper, und&lt;br /&gt;
genau dort erwartet sie sie. Geschrieben werden sie deshalb wie jedes andere&lt;br /&gt;
Feld, nur eben mit dem Writer.}}&lt;br /&gt;
&lt;br /&gt;
Rückgabewerte:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! status !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| 1      || Erfolgreich, der Server erzeugt aus den &#039;&#039;_OBS_JWT_*&#039;&#039;-Werten einen Token (HS256)&lt;br /&gt;
|-&lt;br /&gt;
| 9      || Misserfolg, der Server antwortet mit &#039;&#039;&#039;401&#039;&#039;&#039; und reicht den Wert von &#039;&#039;error&#039;&#039; als &#039;&#039;error.message&#039;&#039; an den Client durch (zusätzlich protokolliert)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Text aus &#039;&#039;error&#039;&#039; geht bei &#039;&#039;status&#039;&#039; = 9 &#039;&#039;&#039;an den Konsumenten&#039;&#039;&#039;&lt;br /&gt;
und wird typischerweise direkt unter dem Passwortfeld angezeigt. Er sollte für&lt;br /&gt;
Endanwender verständlich sein und keine internen Details verraten.}}&lt;br /&gt;
&lt;br /&gt;
Liefert das Skript einen anderen Status als 1 oder 9, ist gar nicht lauffähig oder&lt;br /&gt;
gibt kein auswertbares JSON zurück, antwortet der Server mit &#039;&#039;&#039;403&#039;&#039;&#039; und einer&lt;br /&gt;
generischen Meldung - ein defektes Anmeldeskript soll dem Anwender nicht als&lt;br /&gt;
„Passwort falsch&amp;quot; erscheinen.&lt;br /&gt;
&lt;br /&gt;
Kann der Server die &#039;&#039;&#039;Sitzungszeile&#039;&#039;&#039; zum ausgestellten Token nicht schreiben&lt;br /&gt;
(Tabelle fehlt, Datenbankproblem), liefert er &#039;&#039;&#039;kein Token&#039;&#039;&#039; aus und antwortet&lt;br /&gt;
mit &#039;&#039;&#039;503&#039;&#039;&#039; und &#039;&#039;Retry-After&#039;&#039;. Auch das ist bewusst kein 401/403: die&lt;br /&gt;
Zugangsdaten waren richtig, der Versuch darf wiederholt werden.&lt;br /&gt;
&lt;br /&gt;
===Aufbau der Token-Antwort===&lt;br /&gt;
&lt;br /&gt;
Bei Erfolg baut der Server die Antwort aus den Token-Feldern &#039;&#039;&#039;und allen weiteren&lt;br /&gt;
Feldern, die das Skript zurückgibt&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;benutzerId&amp;quot;: &amp;quot;4711&amp;quot;, &amp;quot;tenant&amp;quot;: &amp;quot;nord&amp;quot;, &amp;quot;roles&amp;quot;: [&amp;quot;TECHNIKER&amp;quot;],&lt;br /&gt;
  &amp;quot;token&amp;quot;:       &amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;accessToken&amp;quot;: &amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;refreshToken&amp;quot;:&amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;:   28800,&lt;br /&gt;
  &amp;quot;serverTime&amp;quot;:  &amp;quot;2026-08-17T09:12:33+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld !! Herkunft&lt;br /&gt;
|-&lt;br /&gt;
| token / accessToken || Derselbe Access-Token unter zwei Namen. &#039;&#039;accessToken&#039;&#039; erwarten die meisten Client-Bibliotheken, &#039;&#039;token&#039;&#039; bleibt für bestehende Konsumenten erhalten&lt;br /&gt;
|-&lt;br /&gt;
| refreshToken || Nur wenn das Skript &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039; geliefert hat&lt;br /&gt;
|-&lt;br /&gt;
| expiresIn || Laufzeit des Access-Tokens in &#039;&#039;&#039;Sekunden&#039;&#039;&#039; (&#039;&#039;JWT-Exp&#039;&#039; × 60)&lt;br /&gt;
|-&lt;br /&gt;
| serverTime || Serverzeit ISO-8601 mit Offset&lt;br /&gt;
|-&lt;br /&gt;
| alle übrigen Felder || &#039;&#039;&#039;Frei vom Skript bestimmt.&#039;&#039;&#039; Übernommen wird alles ausser &#039;&#039;status&#039;&#039; und den &#039;&#039;_OBS_&#039;&#039;-Steuerfeldern&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Damit kann das Anmeldeskript alles mitliefern, was der Client direkt nach dem Login&lt;br /&gt;
braucht (Benutzer-Id, Mandant, Rollen, Berechtigungen) - ohne dass dieser dafür&lt;br /&gt;
einen zweiten Aufruf absetzen muss. Ein Feld, das genauso heisst wie eines der&lt;br /&gt;
Token-Felder oben, wird verworfen; die Engine setzt diese selbst.&lt;br /&gt;
&lt;br /&gt;
===Custom-Claims, serverTime und Refresh===&lt;br /&gt;
&lt;br /&gt;
Das Authenticate-Skript kann dem Token zusätzlich &#039;&#039;&#039;beliebige Custom-Claims&#039;&#039;&#039; mitgeben - z.B. Mandant und Rollen - und optional ein &#039;&#039;&#039;Refresh-Token&#039;&#039;&#039; anstoßen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! RÜckgabefeld !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| _OBS_JWT_CLAIM_&amp;amp;lt;name&amp;amp;gt; || Beliebiger Custom-Claim, z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles. Im Folge-Skript lesbar als &amp;lt;code&amp;gt;oReader.Claim(&#039;&amp;amp;lt;name&amp;amp;gt;&#039;)&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| _OBS_JWT_REFRESH_ID || (optional) jti des Refresh-Tokens. Nur wenn gesetzt, stellt der Server ein Refresh-Token aus.&lt;br /&gt;
|-&lt;br /&gt;
| _OBS_JWT_REFRESH_EXP || (optional) Lebensdauer des Refresh-Tokens in Minuten (Default 90 Tage = 129600).&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Jedes Token trägt automatisch den Claim &#039;&#039;token_use&#039;&#039; (&#039;&#039;access&#039;&#039; bzw. &#039;&#039;refresh&#039;&#039;) sowie die Sitzungskennungen &#039;&#039;sid&#039;&#039; und &#039;&#039;rid&#039;&#039; des Servers. Die Antwort enthält bei Erfolg &#039;&#039;serverTime&#039;&#039; (ISO-8601 mit Offset), bei ausgestelltem Refresh zusätzlich &#039;&#039;refreshToken&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;token&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;refreshToken&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;serverTime&amp;quot;:&amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Mandant und Rollen immer aus dem Token lesen (&#039;&#039;oReader.Claim(…)&#039;&#039;), nie aus Body oder Query.}}&lt;br /&gt;
&lt;br /&gt;
====Refresh-Methode====&lt;br /&gt;
&lt;br /&gt;
Der Refresh läuft über &#039;&#039;&#039;denselben JWT-Endpunkt&#039;&#039;&#039; und &#039;&#039;&#039;dasselbe Skript&#039;&#039;&#039;. Liegt am JWT-Endpunkt ein &#039;&#039;Authorization: Bearer &amp;lt;token&amp;gt;&#039;&#039; mit einem Refresh-Token vor, ruft der Server statt &#039;&#039;Authenticate&#039;&#039; die Methode &#039;&#039;&#039;Refresh&#039;&#039;&#039; auf (ohne Bearer: Login; &#039;&#039;DELETE&#039;&#039; mit Access-Token: Abmelden, ohne Skript).&lt;br /&gt;
&lt;br /&gt;
Bevor das Skript läuft, hat der Server das vorgelegte Refresh-Token verifiziert&lt;br /&gt;
(Signatur, Ablauf, &#039;&#039;token_use=refresh&#039;&#039;) und in der Sitzung &#039;&#039;&#039;entwertet&#039;&#039;&#039;.&lt;br /&gt;
Einmalgebrauch, Rotation und Sperrliste sind damit Sache des Servers. Das Skript&lt;br /&gt;
liefert nur noch die aktuellen Claims - genau das ist der Sinn des Aufrufs: eine&lt;br /&gt;
zwischenzeitlich geänderte Rolle oder ein gewechselter Mandant wirken spätestens&lt;br /&gt;
mit der nächsten Erneuerung.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Eine &#039;&#039;&#039;eigene Sperrtabelle im Skript ist nicht mehr nötig&#039;&#039;&#039; und sollte&lt;br /&gt;
entfernt werden. Wer sie weiterführt, rotiert zweimal - einmal im Skript, einmal im&lt;br /&gt;
Server - und riskiert, dass beide Seiten unterschiedlicher Meinung sind. Der&lt;br /&gt;
Server sieht die Skript-Tabelle nicht, und das Skript sieht die Sitzung nicht.}}&lt;br /&gt;
&lt;br /&gt;
Ein bereits benutztes Refresh-Token wird mit &#039;&#039;&#039;401&#039;&#039;&#039; abgelehnt. Erfolgt die&lt;br /&gt;
erneute Vorlage innerhalb von 60 Sekunden, bleibt die Sitzung bestehen (paralleler&lt;br /&gt;
Refresh einer App); danach gilt sie als Wiedervorlage und die &#039;&#039;&#039;gesamte Sitzung&lt;br /&gt;
wird gesperrt&#039;&#039;&#039;. Details in [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
Das alte Refresh-jti steht weiterhin als &#039;&#039;oReader.Param(&#039;_OBS_JWT_ID&#039;)&#039;&#039; bereit,&lt;br /&gt;
falls das Skript es für eigene Protokollierung braucht.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Refresh(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUser: string;&lt;br /&gt;
begin&lt;br /&gt;
    cUser := oReader.Subject();&lt;br /&gt;
&lt;br /&gt;
    // Der Server hat das vorgelegte Refresh-Token bereits geprueft und&lt;br /&gt;
    // entwertet. Hier wird nur entschieden, ob der Benutzer die Sitzung&lt;br /&gt;
    // fortsetzen darf - und mit welchen Rechten.&lt;br /&gt;
    if (not BenutzerAktiv(cUser)) then begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;, 9);&lt;br /&gt;
        oWriter.Str(&#039;error&#039; , &#039;Konto ist nicht mehr aktiv, bitte neu anmelden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // Rolle und Mandant neu lesen, nicht aus dem alten Token uebernehmen -&lt;br /&gt;
    // sonst wirkt ein Rechteentzug erst beim naechsten Login.&lt;br /&gt;
    oWriter.Int(&#039;status&#039;               , 1);&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_ID&#039;          , cUser);&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_SUBJECT&#039;     , cUser);&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_CLAIM_tenant&#039;, TechnikerMandant(cUser));&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_CLAIM_roles&#039; , TechnikerRollen(cUser));&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_REFRESH_ID&#039;  , GlobalUID());   // loest das neue Refresh-Token aus&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Abmelden (Logout)===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Für das Abmelden braucht das Skript keine Methode.&#039;&#039;&#039; Es läuft über ein&lt;br /&gt;
&#039;&#039;DELETE&#039;&#039; auf denselben JWT-Endpunkt (Access-Token im &#039;&#039;Authorization&#039;&#039;-Header)&lt;br /&gt;
und wird komplett in der Engine erledigt: die Sitzung wird gesperrt, die Antwort&lt;br /&gt;
ist 204. Eine Methode &#039;&#039;Logout&#039;&#039; gibt es nicht und wird nicht aufgerufen.&lt;br /&gt;
Details: [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]], Phase 4.&lt;br /&gt;
&lt;br /&gt;
Wer beim Abmelden zusätzlich fachlich etwas tun will - eine Geräteregistrierung&lt;br /&gt;
löschen, einen Push-Token verwerfen, den Vorgang protokollieren - nimmt einen&lt;br /&gt;
&#039;&#039;&#039;gewöhnlichen Endpunkt&#039;&#039;&#039; und ruft dort &#039;&#039;oWriter.JwtRevoke&#039;&#039;. Dasselbe gilt für&lt;br /&gt;
&amp;quot;auf allen Geräten abmelden&amp;quot;, weil das eine Berechtigungsentscheidung braucht:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    GeraeteRegistrierungLoeschen(oReader.Subject());&lt;br /&gt;
&lt;br /&gt;
    // &#039;session&#039; = diese Sitzung, &#039;all&#039; = alle Sitzungen des Subjects&lt;br /&gt;
    oWriter.JwtRevoke(&#039;session&#039;);&lt;br /&gt;
    oWriter.Status(204);&lt;br /&gt;
    oWriter.NoBody();&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Gesperrt wird in beiden Wegen die &#039;&#039;&#039;ganze Sitzung&#039;&#039;&#039;, also auch die noch nicht&lt;br /&gt;
abgelaufenen Access-Token vorheriger Erneuerungen. Ein wiederholter Aufruf ist&lt;br /&gt;
unschädlich. Lässt sich die Sitzung nicht sperren, überschreibt der Server die&lt;br /&gt;
Antwort des Skripts mit &#039;&#039;&#039;503&#039;&#039;&#039; - ein Client darf nicht glauben, er sei&lt;br /&gt;
abgemeldet, während sein Token weiterläuft.&lt;br /&gt;
&lt;br /&gt;
==Fehlerbehandlung im Skript==&lt;br /&gt;
&lt;br /&gt;
Tritt im Skript eine Exception auf oder schlägt die Syntax-Prüfung fehl, antwortet der Server mit 500 &#039;&#039;Interner Fehler&#039;&#039; und protokolliert die Detail-Meldung in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; (mit Skript-Fehlertext). Der Konsument sieht keine internen Details.&lt;br /&gt;
&lt;br /&gt;
Server-eigene Fehler (Routing, Rate-Limit, Token, Auffangnetz) haben ein&lt;br /&gt;
einheitliches Format mit einem &#039;&#039;error&#039;&#039;-Objekt - Aufbau siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer|Übersicht]].&lt;br /&gt;
&lt;br /&gt;
Für einen spezifischen Fehler an den Konsumenten gibt es &#039;&#039;&#039;einen&#039;&#039;&#039; Aufruf. Er&lt;br /&gt;
baut dieselbe Hülle, die auch die Server-eigenen Fehler tragen - Statuscode,&lt;br /&gt;
&#039;&#039;code&#039;&#039;, &#039;&#039;message&#039;&#039;, &#039;&#039;uid&#039;&#039; und &#039;&#039;traceId&#039;&#039; -, sodass beide Wege nicht&lt;br /&gt;
auseinanderlaufen können:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    if (Empty(oReader.Param(&#039;kundennr&#039;))) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Parameter &amp;quot;kundennr&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
    // ...&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Error verwirft einen schon begonnenen Körper&#039;&#039;&#039; und setzt seinen&lt;br /&gt;
eigenen. Ein Skript darf also getrost Felder schreiben und danach noch&lt;br /&gt;
abbrechen - was der Client sieht, ist die Fehlerantwort. Nur ein &#039;&#039;&#039;offener&lt;br /&gt;
Stapel&#039;&#039;&#039; (ein &#039;&#039;ObjBegin&#039;&#039; ohne &#039;&#039;ObjEnd&#039;&#039;) bleibt auch dann ein Strukturfehler.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein fachlicher Fehler gehört auf einen &#039;&#039;&#039;4xx&#039;&#039;&#039;-Statuscode, nicht auf&lt;br /&gt;
200 mit Fehlertext im Body. Nur so erkennt der Konsument den Fehlschlag ohne den&lt;br /&gt;
Body auszuwerten - und nur so gibt der Server einen belegten&lt;br /&gt;
&#039;&#039;Idempotency-Key&#039;&#039; wieder frei (siehe Abschnitt Idempotenz).}}&lt;br /&gt;
&lt;br /&gt;
===Ändern: qSqlInit statt qSqlRead===&lt;br /&gt;
&lt;br /&gt;
Zum &#039;&#039;&#039;Ändern&#039;&#039;&#039; eines vorhandenen Satzes wird ebenfalls &amp;lt;code&amp;gt;qSqlInit&amp;lt;/code&amp;gt;&lt;br /&gt;
benutzt - der Satz wird über &amp;lt;code&amp;gt;sys_uid&amp;lt;/code&amp;gt; adressiert:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
q := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
q.qSet(&#039;sys_uid&#039;  , cSysUid);      // Schreib-Index auf den Originalsatz&lt;br /&gt;
q.qSet(&#039;ti_status&#039;, &#039;9&#039;);&lt;br /&gt;
if (not q.SaveData(UPDATE_RECORD)) then begin&lt;br /&gt;
    // ... Fehler behandeln, siehe unten&lt;br /&gt;
end;&lt;br /&gt;
qSqlFree(q);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;qSqlRead&amp;lt;/code&amp;gt; ist dafür die falsche Wahl, aus zwei Gründen:&lt;br /&gt;
&lt;br /&gt;
* Es liest den &#039;&#039;&#039;kompletten Altsatz&#039;&#039;&#039; ein - ein zusätzlicher Lesezugriff, der Zeit kostet.&lt;br /&gt;
* Es schreibt anschliessend nur die &#039;&#039;&#039;Änderungen&#039;&#039;&#039;. Ist der neue Wert gleich dem alten, entsteht gar kein Write, und &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt; liefert &#039;&#039;&#039;False&#039;&#039;&#039; - ununterscheidbar von einem echten Fehlschlag.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ohne &amp;lt;code&amp;gt;sys_uid&amp;lt;/code&amp;gt; legt &amp;lt;code&amp;gt;qSqlInit&amp;lt;/code&amp;gt; einen &#039;&#039;&#039;neuen&#039;&#039;&#039;&lt;br /&gt;
Datensatz an, statt den vorhandenen zu ändern. Die &amp;lt;code&amp;gt;sys_uid&amp;lt;/code&amp;gt; ist der&lt;br /&gt;
Schreib-Index und gehört bei jeder Änderung gesetzt.}}&lt;br /&gt;
&lt;br /&gt;
Damit entfällt auch das übliche Paar aus &amp;lt;code&amp;gt;DB_LSeek&amp;lt;/code&amp;gt; und&lt;br /&gt;
&amp;lt;code&amp;gt;qSqlRead&amp;lt;/code&amp;gt;: &#039;&#039;&#039;eine&#039;&#039;&#039; Abfrage auf die &amp;lt;code&amp;gt;sys_uid&amp;lt;/code&amp;gt; liefert&lt;br /&gt;
zugleich die Existenzprüfung und den Schreib-Index.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
cUid := &#039;&#039;;&lt;br /&gt;
if (DB_SOpen(oDB, &#039;SELECT sys_uid FROM tickets WHERE &#039; + cWhere + &#039; LIMIT 1&#039;, q)) then begin&lt;br /&gt;
    if (not q.EoF) then begin&lt;br /&gt;
        cUid := q.A2UID();&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
DB_Close(q);&lt;br /&gt;
&lt;br /&gt;
if (not Empty(cUid)) then begin&lt;br /&gt;
    // ändern: qSqlInit + sys_uid&lt;br /&gt;
end else begin&lt;br /&gt;
    // anlegen: qSqlInit ohne sys_uid&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Mehrere Felder sind EIN Schreibvorgang===&lt;br /&gt;
&lt;br /&gt;
Ein &amp;lt;code&amp;gt;qSqlInit&amp;lt;/code&amp;gt;-Objekt nimmt beliebig viele &amp;lt;code&amp;gt;qSet&amp;lt;/code&amp;gt; entgegen&lt;br /&gt;
und schreibt sie mit &#039;&#039;&#039;einem&#039;&#039;&#039; &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt;. Fallen an derselben&lt;br /&gt;
Zeile mehrere Felder an, gehören sie in dasselbe Objekt:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
// FALSCH: fünf Felder, fünf Schreibvorgänge - und jeder Setzer, der sich&lt;br /&gt;
// seine sys_uid selbst holt, bringt zusätzlich eine eigene Abfrage mit&lt;br /&gt;
FeldSetzen(cUid, &#039;ti_status&#039; , &#039;9&#039;);&lt;br /&gt;
FeldSetzen(cUid, &#039;ti_grund&#039;  , cGrund);&lt;br /&gt;
FeldSetzen(cUid, &#039;ti_von&#039;    , cBenutzer);&lt;br /&gt;
&lt;br /&gt;
// RICHTIG: ein Schreibobjekt, ein SaveData&lt;br /&gt;
q := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
q.qSet(&#039;sys_uid&#039;  , cUid);&lt;br /&gt;
q.qSet(&#039;ti_status&#039;, &#039;9&#039;);&lt;br /&gt;
q.qSet(&#039;ti_grund&#039; , cGrund);&lt;br /&gt;
q.qSet(&#039;ti_von&#039;   , cBenutzer);&lt;br /&gt;
if (not q.SaveData(UPDATE_RECORD)) then begin&lt;br /&gt;
    // ... Fehler behandeln&lt;br /&gt;
end;&lt;br /&gt;
qSqlFree(q);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Das ist nicht nur eine Frage der Geschwindigkeit: einzeln geschriebene Felder&lt;br /&gt;
sind &#039;&#039;&#039;nicht atomar&#039;&#039;&#039;. Bricht der dritte Schreibvorgang ab, steht die Zeile&lt;br /&gt;
halb geändert da - Grund gesetzt, Zeitpunkt nicht.&lt;br /&gt;
&lt;br /&gt;
Wird nichts gesetzt, darf auch nicht geschrieben werden: das Schreibobjekt dann&lt;br /&gt;
mit &amp;lt;code&amp;gt;qSqlFree&amp;lt;/code&amp;gt; freigeben, statt ein leeres &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt;&lt;br /&gt;
abzusetzen.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein Setzer für &#039;&#039;&#039;ein&#039;&#039;&#039; Feld (&amp;lt;code&amp;gt;FeldSetzen(cUid, cFeld, cWert)&amp;lt;/code&amp;gt;)&lt;br /&gt;
ist bequem und deshalb verführerisch. Er versteckt aber je Aufruf eine eigene&lt;br /&gt;
Abfrage und ein eigenes &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt;. Für einen Vorgang mit mehreren&lt;br /&gt;
Feldern gehört ein &#039;&#039;&#039;Editier-Helfer&#039;&#039;&#039; her, der das Schreibobjekt liefert -&lt;br /&gt;
geschrieben wird einmal am Ende.}}&lt;br /&gt;
&lt;br /&gt;
===Keine Abfrage je Zeile===&lt;br /&gt;
&lt;br /&gt;
Was in einer Liste je Zeile nachgeschlagen wird, kostet &#039;&#039;&#039;N&#039;&#039;&#039; Abfragen für&lt;br /&gt;
&#039;&#039;&#039;eine&#039;&#039;&#039; Antwort. Bei fünfzig Aufträgen sind das fünfzig Abfragen für ein&lt;br /&gt;
einziges Feld. Der Ausweg ist immer derselbe:&lt;br /&gt;
&lt;br /&gt;
* Werte aus einer 1:1-Beziehung über einen &amp;lt;code&amp;gt;LEFT JOIN&amp;lt;/code&amp;gt; mitnehmen - bei einem &#039;&#039;&#039;eindeutigen&#039;&#039;&#039; Index auf dem Join-Feld kann er die Zeilen nicht vervielfachen.&lt;br /&gt;
* Einzelwerte aus einer 1:n-Beziehung über eine &#039;&#039;&#039;Unterabfrage&#039;&#039;&#039; in der SELECT-Liste holen, nicht über einen Join (der Join würde die Zeilen vervielfachen).&lt;br /&gt;
* Eine echte Unterliste (Positionen, Dateien) nur dann nachladen, wenn ein &#039;&#039;&#039;Zählfeld&#039;&#039;&#039; in der Hauptabfrage sagt, dass es überhaupt eine gibt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein GET soll &#039;&#039;&#039;lesen&#039;&#039;&#039;. Schreibvorgänge im Lesepfad - etwas&lt;br /&gt;
&amp;quot;beim ersten Ausliefern festschreiben&amp;quot; - vervielfachen sich mit der Zeilenzahl&lt;br /&gt;
und treffen den Aufrufer, der nur eine Liste angefordert hat.}}&lt;br /&gt;
&lt;br /&gt;
===Schreibfehler erkennen===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;&amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt; liefert einen Boolean - und der ist die einzige&lt;br /&gt;
Fehlermeldung, die es gibt.&#039;&#039;&#039; (Auch der &#039;&#039;&#039;Parameter&#039;&#039;&#039; ist ein Boolean:&lt;br /&gt;
&#039;&#039;NEW_RECORD&#039;&#039; und &#039;&#039;UPDATE_RECORD&#039;&#039; sind Wahrheitswerte, keine Zahlen.) Schlägt das Schreiben in der Datenbank fehl&lt;br /&gt;
(&#039;&#039;Duplicate entry&#039;&#039; auf einem eindeutigen Index, zu langer Wert, fehlende&lt;br /&gt;
Spalte), dann&lt;br /&gt;
&lt;br /&gt;
* wird &#039;&#039;&#039;keine&#039;&#039;&#039; Exception ausgelöst,&lt;br /&gt;
* steht &#039;&#039;&#039;nichts&#039;&#039;&#039; in &#039;&#039;RESTSRV_PROTO&#039;&#039;,&lt;br /&gt;
* läuft das Skript weiter und antwortet mit dem Erfolg, den es selbst formuliert.&lt;br /&gt;
&lt;br /&gt;
Wer den Aufruf als Anweisung schreibt, baut damit einen stillen Datenverlust ein.&lt;br /&gt;
Weil in einem Endpunkt schnell ein Dutzend Schreibvorgänge zusammenkommen, gehört&lt;br /&gt;
die Prüfung in eine eigene kleine Prozedur - am besten in die gemeinsame Lib:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Speichern(q: TqSQL; lNeu: Boolean; const cWas: string);&lt;br /&gt;
var lOk: Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    lOk := q.SaveData(lNeu);&lt;br /&gt;
    qSqlFree(q);                     // auch im Fehlerfall freigeben&lt;br /&gt;
    if (not lOk) then begin&lt;br /&gt;
        raise Exception.Create(&#039;Schreiben fehlgeschlagen: &#039; + cWas);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
// Aufruf statt SaveData + qSqlFree:&lt;br /&gt;
q := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
q.qSet(&#039;ti_betreff&#039;, cBetreff);&lt;br /&gt;
Speichern(q, NEW_RECORD, &#039;Ticket&#039;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Exception fängt der Server: er schreibt &#039;&#039;Schreiben fehlgeschlagen: Ticket&#039;&#039;&lt;br /&gt;
nach &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; und antwortet mit &#039;&#039;&#039;500&#039;&#039;&#039;. Liegt ein&lt;br /&gt;
&#039;&#039;Idempotency-Key&#039;&#039; an, wird er dabei &#039;&#039;&#039;freigegeben&#039;&#039;&#039; - die Anfrage ist also&lt;br /&gt;
wiederholbar (siehe Abschnitt Idempotenz).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein fehlgeschlagenes Schreiben ist kein Fall, in dem ein Endpunkt&lt;br /&gt;
sinnvoll weiterlaufen kann: die Fachwirkung ist dann unvollständig. Wer trotzdem&lt;br /&gt;
weitermachen will - etwa weil mehrere unabhängige Sätze geschrieben werden -,&lt;br /&gt;
prüft den Rückgabewert und baut selbst eine Antwort. Ignorieren darf man ihn&lt;br /&gt;
nie.}}&lt;br /&gt;
&lt;br /&gt;
Gängige Codes, die auch der Server selbst verwendet: &#039;&#039;VALIDATION_FAILED&#039;&#039; (422),&lt;br /&gt;
&#039;&#039;NOT_FOUND&#039;&#039; (404), &#039;&#039;FORBIDDEN_ROLE&#039;&#039; (403), &#039;&#039;VERSION_CONFLICT&#039;&#039; (409).&lt;br /&gt;
Eigene, fachlich sprechende Codes sind erlaubt - wichtig ist, dass sie stabil&lt;br /&gt;
bleiben, weil Konsumenten darauf ihre Reaktion aufbauen.&lt;br /&gt;
&lt;br /&gt;
==Gemeinsamen Code auslagern==&lt;br /&gt;
&lt;br /&gt;
Wird Logik in mehreren Endpunkten gebraucht - Berechtigungsprüfungen,&lt;br /&gt;
JSON-Bausteine, Hilfsfunktionen -, muss sie nicht in jedes Skript kopiert werden.&lt;br /&gt;
Die Skript-Engine lädt Textbausteine beim Laden aus einer beliebigen Tabelle nach:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
{$L T=&amp;quot;restsrv_endpoints&amp;quot; I=&amp;quot;re_pathtemplate&amp;quot; V=&amp;quot;/meinapp/lib&amp;quot; F=&amp;quot;re_script&amp;quot;}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Attribut !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;T&#039;&#039;&#039; || Tabelle, aus der geladen wird&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;I&#039;&#039;&#039; || Spalte, über die gesucht wird&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;V&#039;&#039;&#039; || Wert, der in dieser Spalte stehen muss&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;F&#039;&#039;&#039; || Spalte, deren Inhalt an dieser Stelle eingesetzt wird&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die Direktive steht üblicherweise direkt unter dem Kopfkommentar des Skripts. Der&lt;br /&gt;
Compiler sieht danach den zusammengesetzten Text - eine Funktion aus der Lib lässt&lt;br /&gt;
sich also aufrufen wie eine im Skript selbst geschriebene.&lt;br /&gt;
&lt;br /&gt;
===Bewährtes Muster: Lib als eigene Endpunkt-Zeile===&lt;br /&gt;
&lt;br /&gt;
Am wenigsten Verwaltung macht es, die Lib &#039;&#039;&#039;als eigene Zeile in&lt;br /&gt;
RESTSRV_ENDPOINTS&#039;&#039;&#039; abzulegen:&lt;br /&gt;
&lt;br /&gt;
# Endpunkt anlegen mit einem eigenen Pfad-Template, z.B. &amp;lt;code&amp;gt;/meinapp/lib&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Den Lib-Quelltext ins Skript-Feld dieser Zeile schreiben (F7).&lt;br /&gt;
# &#039;&#039;&#039;Keine Berechtigung&#039;&#039;&#039; in der Berechtigungs-Liste vergeben - das ist der Schutz: ohne Berechtigung beantwortet der Server einen Aufruf mit 403.&lt;br /&gt;
# In jedem nutzenden Endpunkt die Direktive oben einfügen.&lt;br /&gt;
&lt;br /&gt;
Damit ist die Lib reiner Ablageort und von aussen nicht nutzbar. Eine Änderung&lt;br /&gt;
wirkt auf alle einbindenden Endpunkte, ohne dass ein einziges Endpunkt-Skript&lt;br /&gt;
angefasst werden muss.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Lib und Skripte gemeinsam einspielen.&#039;&#039;&#039; Kommt in der Lib eine neue&lt;br /&gt;
Funktion hinzu, genügt es nicht, nur das nutzende Skript zu aktualisieren. Eine&lt;br /&gt;
ältere Lib lädt fehlerfrei - der Compiler meldet dann ausschliesslich die neuen&lt;br /&gt;
Namen als &#039;&#039;Unknown name&#039;&#039;, was leicht wie ein Fehler im Skript aussieht.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Auch hier greift der Endpunkt-Cache: eine Änderung an der Lib wird erst&lt;br /&gt;
nach Ablauf des TTL (bis zu 60 Sekunden) wirksam.}}&lt;br /&gt;
&lt;br /&gt;
===Wohin gehört welcher Code?===&lt;br /&gt;
&lt;br /&gt;
Drei Ebenen, und die Zuordnung entscheidet, wie schnell eine Änderung wirkt:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Art des Codes !! Wohin !! Wirksam&lt;br /&gt;
|-&lt;br /&gt;
| Fachlogik **einer** Anwendung - Feldnamen, Zustandswerte, Rollenregeln, Schreibziele || in die &#039;&#039;&#039;DWS-Lib dieser Anwendung&#039;&#039;&#039; (eigene Endpunkt-Zeile, per &amp;lt;code&amp;gt;{$L …}&amp;lt;/code&amp;gt; eingebunden) || nach Ablauf des Cache-TTL, ohne Build&lt;br /&gt;
|-&lt;br /&gt;
| Mechanik, die &#039;&#039;&#039;mehrere&#039;&#039;&#039; Anwendungen brauchen - und auch das OBS-Umfeld || nach &#039;&#039;&#039;obs_lib&#039;&#039;&#039; als Delphi-Unit || erst nach Build und Rollout&lt;br /&gt;
|-&lt;br /&gt;
| Mechanik, die &#039;&#039;&#039;nur&#039;&#039;&#039; der REST-Server braucht || in die Units des Servers || erst nach Build und Rollout&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die Regel hat einen praktischen Grund und keinen ästhetischen: Fachlogik ändert&lt;br /&gt;
sich in Tagen, Mechanik in Monaten. Was in der Lib liegt, ist nach einer Minute&lt;br /&gt;
aktiv; was in einer Unit liegt, braucht einen Build. Wer Fachlogik nach unten&lt;br /&gt;
schiebt, tauscht Geschwindigkeit gegen nichts ein.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Eine Anwendung, die ihre eigene Lib hat, kopiert sie &#039;&#039;&#039;nicht&#039;&#039;&#039; von&lt;br /&gt;
einer anderen. Zwei Kopien derselben Lib driften auseinander, und die Abweichung&lt;br /&gt;
fällt erst auf, wenn eine der beiden Anwendungen etwas Falsches tut. Geteiltes&lt;br /&gt;
gehört eine Ebene tiefer.}}&lt;br /&gt;
&lt;br /&gt;
==Fallstricke im Skript==&lt;br /&gt;
&lt;br /&gt;
Die folgenden Punkte unterscheiden das Skript von normalem Delphi-Code und kosten&lt;br /&gt;
sonst eine Runde Fehlersuche:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Stolperstein !! Richtig&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;DB_SOpen(&#039;Y00…&#039;, oDB, …)&#039;&#039; - der AD-UID-Parameter aus dem Delphi-Code || Im Skript &#039;&#039;&#039;ohne UID&#039;&#039;&#039;: &amp;lt;code&amp;gt;DB_SOpen(oDB, cSql, q)&amp;lt;/code&amp;gt;. Gleiches gilt für &amp;lt;code&amp;gt;qSqlInit(oDB, &#039;tab&#039;)&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;qSqlRead(oDB, &#039;tab&#039;, cWhere)&amp;lt;/code&amp;gt; - jede dieser Funktionen beginnt mit &#039;&#039;oDB&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;oReader.Str&amp;amp;lt;T&amp;amp;gt;(…)&#039;&#039; oder irgendein Generic || Generics gibt es in der Skriptsprache nicht. Die Leser sind je Typ eigene Methoden: &#039;&#039;Str&#039;&#039;, &#039;&#039;Int&#039;&#039;, &#039;&#039;Num&#039;&#039;, &#039;&#039;Bool&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Eine eigene Prozedur &#039;&#039;Delete&#039;&#039; für DELETE || &#039;&#039;Delete&#039;&#039; ist eine &#039;&#039;&#039;Standardprozedur&#039;&#039;&#039; der Sprache. Die Deklaration lässt sich nicht übersetzen, und die Meldung nennt nur die Zeile - nicht die Ursache&lt;br /&gt;
|-&lt;br /&gt;
| Ein Helfer, der ein JSON-Fragment als String zurückgibt || Den &#039;&#039;&#039;Writer als Parameter&#039;&#039;&#039; übergeben und an der richtigen Stelle hineinschreiben. Fragmente von Hand zusammenzusetzen ist genau der Weg, auf dem die Reihenfolge und die Klammern verloren gingen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;oWriter.ArrEnd&#039;&#039; nach &#039;&#039;RootArr&#039;&#039; || &#039;&#039;RootArr&#039;&#039; öffnet &#039;&#039;&#039;keine&#039;&#039;&#039; Ebene, es bestimmt nur die Klammer der Wurzel. Ein &#039;&#039;ArrEnd&#039;&#039; dazu ist ein Strukturfehler&lt;br /&gt;
|-&lt;br /&gt;
| Zahlen selbst formatieren (&#039;&#039;NStr&#039;&#039;, &#039;&#039;StrTran&#039;&#039;) || &amp;lt;code&amp;gt;oWriter.Num(&#039;preis&#039;, nWert, 2)&amp;lt;/code&amp;gt;. Selbst formatiert stand bei Werten ab 1000 der Tausendertrenner im JSON - gültiges JSON, falscher Wert, keine Meldung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;EMPTY_DATE&#039;&#039; für ein leeres Datum || &#039;&#039;EMPTY_DATE&#039;&#039; ist im Skript ein &#039;&#039;&#039;String&#039;&#039;&#039;. Für Datumsvariablen und -vergleiche &#039;&#039;&#039;MINDATETIME&#039;&#039;&#039; verwenden&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;fVal&#039;&#039; auf einen JSON-Zahlenwert || JSON liefert den Dezimalpunkt, &#039;&#039;fVal&#039;&#039; erwartet die lokale Notation: vorher &amp;lt;code&amp;gt;StrTran(cVal, &#039;.&#039;, &#039;,&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;TStringList&#039;&#039; für Zwischenlisten || In Skripten unzuverlässig. Kleine Mengen über einen Delimiter-String führen (&amp;lt;code&amp;gt;&#039;&amp;amp;#124;&#039; + wert + &#039;&amp;amp;#124;&#039;&amp;lt;/code&amp;gt;) und mit &#039;&#039;Pos&#039;&#039; prüfen&lt;br /&gt;
|-&lt;br /&gt;
| Default-Parameter in eigenen Funktionen || Werden nicht unterstützt - alle Parameter ausschreiben&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;q.SaveData(…)&amp;lt;/code&amp;gt; als Anweisung schreiben || &#039;&#039;SaveData&#039;&#039; liefert einen &#039;&#039;&#039;Boolean&#039;&#039;&#039;. Ein SQL-Fehler beim Schreiben wirft &#039;&#039;&#039;keine&#039;&#039;&#039; Exception und landet in &#039;&#039;&#039;keinem&#039;&#039;&#039; Protokoll - der Rückgabewert ist die einzige Meldung. Immer auswerten, siehe [[#Schreibfehler erkennen|Schreibfehler erkennen]]&lt;br /&gt;
|-&lt;br /&gt;
| Je Feld ein eigener Setzer-Aufruf || Ein &amp;lt;code&amp;gt;qSqlInit&amp;lt;/code&amp;gt;-Objekt, beliebig viele &amp;lt;code&amp;gt;qSet&amp;lt;/code&amp;gt;, &#039;&#039;&#039;ein&#039;&#039;&#039; &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt;. Einzeln geschrieben kostet jedes Feld eine eigene Abfrage samt eigenem Write - und der Vorgang ist nicht mehr atomar, siehe [[#Mehrere Felder sind EIN Schreibvorgang|Mehrere Felder]]&lt;br /&gt;
|-&lt;br /&gt;
| Ein Wert je Zeile nachgeschlagen || In einer Liste sind das N Abfragen für eine Antwort. Über &amp;lt;code&amp;gt;LEFT JOIN&amp;lt;/code&amp;gt; (1:1) oder Unterabfrage (1:n) mitnehmen, siehe [[#Keine Abfrage je Zeile|Keine Abfrage je Zeile]]&lt;br /&gt;
|-&lt;br /&gt;
| Ausgabeparameter vom Typ &amp;lt;code&amp;gt;var … : TDateTime&amp;lt;/code&amp;gt; || Bringt die Skript-VM zum &#039;&#039;&#039;Absturz&#039;&#039;&#039; (Assertion in der Skript-VM), und zwar beim ersten &#039;&#039;&#039;Lesen&#039;&#039;&#039; des Werts - nicht schon beim Schreiben, die Ursache liegt also nicht dort, wo der Fehler auffällt. &amp;lt;code&amp;gt;var string&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;var Integer&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;var Double&amp;lt;/code&amp;gt; laufen. Zeitpunkte als ISO-Zeichenkette zurückgeben und erst beim Aufrufer wandeln&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Skript-Cache==&lt;br /&gt;
&lt;br /&gt;
Der Server cached das kompilierte Skript pro Endpunkt (Schlüssel: &#039;&#039;sys_date&#039;&#039; des Endpunkts). Zusätzlich werden die Endpunkt-Definitionen pro Server-Profil in einem TTL-Cache (Standard 60 s) gehalten. Eine Skript-Änderung über F7 wird daher erst nach Ablauf dieses TTL (bzw. nach einer Cache-Invalidierung) wirksam - typischerweise innerhalb einer Minute, nicht zwingend sofort.&lt;br /&gt;
&lt;br /&gt;
==Verfügbare Bibliotheken==&lt;br /&gt;
&lt;br /&gt;
Im Skript können alle OBS-Standard-Bibliotheken verwendet werden. Typische Einstiegspunkte:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Base.Tools&#039;&#039; - String-, Datum-, IIF-, Empty-Helper&lt;br /&gt;
* &#039;&#039;Base.DB&#039;&#039; / &#039;&#039;Base.xQuery&#039;&#039; - Datenbank-Operationen&lt;br /&gt;
* &#039;&#039;Base.qSqlReg&#039;&#039; - Schreibzugriffe&lt;br /&gt;
* &#039;&#039;lib_ScriptIO&#039;&#039; - Writer und Reader, siehe unten. &#039;&#039;&#039;System.JSON braucht ein Endpunkt-Skript nicht&#039;&#039;&#039; - der Writer erzeugt die Antwort, der Reader liest den Körper&lt;br /&gt;
* &#039;&#039;Base.ToolsConst&#039;&#039; - Konstanten wie &#039;&#039;CRLF&#039;&#039;, &#039;&#039;SINGELQUOTE&#039;&#039;, &#039;&#039;MINDATETIME&#039;&#039;&lt;br /&gt;
* &#039;&#039;lib_ScriptIO&#039;&#039; - &#039;&#039;TxRestWriter&#039;&#039;, &#039;&#039;TxRestReader&#039;&#039; und die Zeitfunktionen &#039;&#039;RestIsoFromDate&#039;&#039; / &#039;&#039;RestDateFromIso&#039;&#039; / &#039;&#039;RestOffsetToMinutes&#039;&#039;. Writer und Reader bekommt das Skript als Parameter; die Zeitfunktionen ruft es direkt&lt;br /&gt;
&lt;br /&gt;
Die wichtigsten Aufrufe mit ihren Skript-Signaturen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Zweck !! Aufruf&lt;br /&gt;
|-&lt;br /&gt;
| Lesen || &amp;lt;code&amp;gt;DB_SOpen(oDB, cSql, q)&amp;lt;/code&amp;gt;, dann &amp;lt;code&amp;gt;q.EoF&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.Next()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.A2C(&#039;feld&#039;)&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;A2I&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;A2F&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;A2D&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;A2UID()&amp;lt;/code&amp;gt;, am Ende &amp;lt;code&amp;gt;DB_Close(q)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Neu anlegen || &amp;lt;code&amp;gt;q := qSqlInit(oDB, &#039;tab&#039;)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.qSet(&#039;feld&#039;, wert)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;if (not q.SaveData(NEW_RECORD)) then …&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;qSqlFree(q)&amp;lt;/code&amp;gt; – der Rückgabewert ist Pflicht, siehe [[#Schreibfehler erkennen|Schreibfehler erkennen]]&lt;br /&gt;
|-&lt;br /&gt;
| Ändern || &amp;lt;code&amp;gt;q := qSqlInit(oDB, &#039;tab&#039;)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.qSet(&#039;sys_uid&#039;, cUid)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.qSet(…)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;if (not q.SaveData(UPDATE_RECORD)) then …&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;qSqlFree(q)&amp;lt;/code&amp;gt; – &#039;&#039;&#039;nicht&#039;&#039;&#039; &amp;lt;code&amp;gt;qSqlRead&amp;lt;/code&amp;gt;, siehe [[#Ändern: qSqlInit statt qSqlRead|Ändern]]. Alle Felder derselben Zeile in &#039;&#039;&#039;ein&#039;&#039;&#039; Objekt, siehe [[#Mehrere Felder sind EIN Schreibvorgang|Mehrere Felder]]&lt;br /&gt;
|-&lt;br /&gt;
| Direkt ausführen || &amp;lt;code&amp;gt;DB_SqlExec(oDB, cSql)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Existenzprüfung || &amp;lt;code&amp;gt;DB_LSeek(oDB, &#039;tab&#039;, cWhere)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Werte quoten (Pflicht) || &amp;lt;code&amp;gt;DB_SQLVal(wert)&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Alle diese Funktionen beginnen im Skript mit &#039;&#039;oDB&#039;&#039;. Ein zusätzlicher&lt;br /&gt;
AD-UID-Parameter als erstes Argument gehört zur Delphi-Variante und lässt sich im&lt;br /&gt;
Skript nicht übersetzen.}}&lt;br /&gt;
&lt;br /&gt;
Konkrete Beispiele:&lt;br /&gt;
&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel1|Beispiel 1: Daten-Abruf mit JWT]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel2|Beispiel 2: Zeiterfassung als komplette Webanwendung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel3|Beispiel 3: Datensatz anlegen mit JSON-Body (CRUD)]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4: Pfad-Parameter im Routing]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel5|Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6: Datei-Upload und Ablage im DMS]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=67341</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Server-Profile</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=67341"/>
		<updated>2026-11-05T12:40:02Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Zertifikats-Pin: zwei Felder, eigener Abschnitt und Validierung ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Server-Profile=&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Server&#039;&#039;&#039; im REST-Modul ist ein eigenständiges TLS-Profil mit einer eigenen HTTP-Server-Instanz. In OBS können beliebig viele Server-Profile parallel betrieben werden, jedes mit eigenem Modus, eigenen Zertifikaten und eigenen Bindungen.&lt;br /&gt;
&lt;br /&gt;
Typische Einsatzfälle für mehrere Profile:&lt;br /&gt;
&lt;br /&gt;
* Ein &#039;&#039;&#039;Public-Profil&#039;&#039;&#039; mit Standard-TLS auf Port 443 für externe Konsumenten.&lt;br /&gt;
* Ein &#039;&#039;&#039;Internal-mTLS-Profil&#039;&#039;&#039; auf einem internen Port für Microservices, die zwingend ein Client-Zertifikat vorlegen müssen.&lt;br /&gt;
* Ein &#039;&#039;&#039;Debug-Profil&#039;&#039;&#039; mit Plain-HTTP auf 127.0.0.1, ausschliesslich für lokale Tests.&lt;br /&gt;
&lt;br /&gt;
==Aufruf==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Server&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Felder des Server-Profils==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Nr                  || Eindeutige Nummer 1-99, einmalig vergeben, kann später nicht mehr geändert werden&lt;br /&gt;
|-&lt;br /&gt;
| Name                || Sprechender Name, eindeutig - erscheint in Protokoll, Statistik und Endpunkt-Zuordnung&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv               || Profil wird beim Start des REST-Dienstes geladen. Inaktive Profile starten nicht&lt;br /&gt;
|-&lt;br /&gt;
| zusätzliche Infos   || Freitext zur Dokumentation (interner Kommentar, wird nicht ausgewertet)&lt;br /&gt;
|-&lt;br /&gt;
| kein SSL            || Plain-HTTP-Modus, ausschliesslich für Debug. TLS-Felder werden ignoriert&lt;br /&gt;
|-&lt;br /&gt;
| mTLS-Modus aktiv    || Mutual TLS: jeder Client muss ein gültiges Client-Zertifikat vorlegen&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikat (cert)   || PEM-Inhalt des Server-Zertifikats (inkl. ggf. Zwischen-CA-Kette)&lt;br /&gt;
|-&lt;br /&gt;
| Schlüssel (key)     || PEM-Inhalt des privaten Schlüssels&lt;br /&gt;
|-&lt;br /&gt;
| Root-Zertifikat (ca) || PEM-Inhalt des CA-Zertifikats, gegen das &#039;&lt;br /&gt;
|-&lt;br /&gt;
| Rate-Limit-Fenster  || Länge des Rate-Limit-Fensters in Sekunden. Leer/0 = Standard 60&lt;br /&gt;
|-&lt;br /&gt;
| Erfolgsanfragen     || Erlaubte erfolgreiche Anfragen je Schlüssel und Fenster. Leer/0 = Standard 120&lt;br /&gt;
|-&lt;br /&gt;
| Fehlanfragen        || Erlaubte fehlgeschlagene Anfragen je Schlüssel und Fenster. Leer/0 = Standard 10&lt;br /&gt;
|-&lt;br /&gt;
| Anfragen            || Erlaubte Anfragen des gesamten Profils je Fenster. Leer/0 = Standard 600&lt;br /&gt;
|-&lt;br /&gt;
| max. Verbindungen   || Obergrenze gleichzeitiger &#039;&#039;&#039;Verbindungen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_conn&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Sie deckt auch laufende Datei-Downloads ab, weil der Platz erst beim Trennen frei wird. Bei Überschreitung trennt der Server &#039;&#039;&#039;ohne HTTP-Antwort&#039;&#039;&#039; - der Wert gehört deutlich über den Normalbetrieb, als Reissleine&lt;br /&gt;
|-&lt;br /&gt;
| max. Anfragen       || Obergrenze gleichzeitig &#039;&#039;&#039;verarbeiteter Anfragen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_parallel&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Bei Überschreitung antwortet der Server &#039;&#039;&#039;503&#039;&#039;&#039; mit &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;, der Client darf es also gleich erneut versuchen&lt;br /&gt;
|-&lt;br /&gt;
| keine Kompression   || Antworten dieses Profils werden &#039;&#039;&#039;nicht&#039;&#039;&#039; gepackt (&amp;lt;code&amp;gt;rsv_no_gzip&amp;lt;/code&amp;gt;). Ohne Haken packt der Server JSON- und Textantworten ab 1024 Byte mit gzip, sofern der Client sie über &amp;lt;code&amp;gt;Accept-Encoding&amp;lt;/code&amp;gt; annimmt. Der Haken betrifft nur die Antwortrichtung - komprimierte &#039;&#039;&#039;Anfragen&#039;&#039;&#039; nimmt der Server weiterhin entgegen&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;max. Verbindungen&#039;&#039; und &#039;&#039;max. Anfragen&#039;&#039; sind die beiden&lt;br /&gt;
&#039;&#039;&#039;Lastgrenzen&#039;&#039;&#039; des Profils. Davon zu unterscheiden sind die festen Grenzen der&lt;br /&gt;
persistenten Verbindungen - 10 Sekunden Leerlauf, 100 Anfragen je Verbindung -,&lt;br /&gt;
die nicht der Last dienen, sondern der Hygiene und nicht einstellbar sind. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer|REST-Server]], Abschnitt Persistente&lt;br /&gt;
Verbindungen.}}&lt;br /&gt;
&lt;br /&gt;
Die Inhalte der Zertifikate werden direkt in OBS als Text gespeichert. Zur Laufzeit schreibt der Server die PEM-Inhalte für den Bruchteil einer Sekunde in ein Temp-Verzeichnis, damit OpenSSL sie beim Start lesen kann; danach werden die Temp-Dateien sofort wieder gelöscht.&lt;br /&gt;
&lt;br /&gt;
==Modi==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Modus               !! Voraussetzungen                       !! Anwendung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039;  || Zertifikat + Schlüssel                || Für den Betrieb, auch wenn der Server nur intern, hinter einem Reverse-Proxy oder per VPN erreichbar ist&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;mTLS&#039;&#039;&#039;          || Zertifikat + Schlüssel + Root-Zert.   || Maschine-zu-Maschine-Kommunikation, in der jeder Client mit Cert validiert wird&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Plain-HTTP&#039;&#039;&#039;    || Haken &#039;&#039;kein SSL&#039;&#039;                    || Ausschliesslich lokale Tests. Erkennt TLS-Versuche auf dem Plain-Port und trennt sie direkt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beide Richtungen werden erkannt: Eine &#039;&#039;&#039;unverschlüsselte Anfrage an einen&lt;br /&gt;
TLS-Port&#039;&#039;&#039; (&#039;&#039;http&#039;&#039; statt &#039;&#039;https&#039;&#039;) beantwortet der Server mit &#039;&#039;&#039;HTTP 400&#039;&#039;&#039;&lt;br /&gt;
und einem Klartext-Hinweis. Die Abweisung erfolgt &#039;&#039;&#039;vor&#039;&#039;&#039; Authentifizierung,&lt;br /&gt;
Rate-Limit und Endpunkt-Skript - ein falsch konfigurierter Client löst also&lt;br /&gt;
keine Verarbeitung aus.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Bei Standard-TLS und mTLS fährt der Server ohne Zertifikat und Schlüssel gar nicht erst hoch. Bei aktiver Erneuerung trägt der Dienst beides selbst ein, mit &#039;&#039;Eigenes Zertifikat (Pin)&#039;&#039; stellt er sich selbst eines aus. Plain-HTTP umgeht TLS komplett und ist nicht für Produktivumgebungen geeignet.}}&lt;br /&gt;
&lt;br /&gt;
==TLS-Version, Cipher und Zertifikats-Rotation==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TLS-Version:&#039;&#039;&#039; Der Server erzwingt &#039;&#039;&#039;TLS 1.2&#039;&#039;&#039;. Die Cipher-Liste ist auf ECDHE-Schlüsselaustausch (Forward Secrecy) mit AEAD-Verfahren (AES-GCM, ChaCha20-Poly1305) beschränkt; CBC, statisches RSA-KEX und DHE sind ausgeschlossen.&lt;br /&gt;
* &#039;&#039;&#039;HSTS:&#039;&#039;&#039; Jede Antwort eines TLS-Profils trägt &#039;&#039;Strict-Transport-Security: max-age=31536000; includeSubDomains&#039;&#039;. Ein Browser spricht die Adresse damit ein Jahr lang nur noch über HTTPS an. Beim Profil &#039;&#039;Kein SSL&#039;&#039; wird der Header bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; gesetzt - der Browser würde ihn sonst speichern und die Erzwingung auch auf produktive Sitzungen anwenden.&lt;br /&gt;
* &#039;&#039;&#039;Rotation:&#039;&#039;&#039; Bei &#039;&#039;&#039;automatischer Erneuerung&#039;&#039;&#039; tauscht der Dienst das Zertifikat selbst und startet dabei nur das betroffene Server-Profil neu; die übrigen Profile und der Dienst laufen weiter. Bei manueller Pflege ändern Sie die PEM-Inhalte und starten den REST-Dienst neu - bis dahin läuft das alte Zertifikat weiter.&lt;br /&gt;
* &#039;&#039;&#039;Certificate-Pinning:&#039;&#039;&#039; Mobile Clients können den Server-Public-Key pinnen. Zertifikatswechsel daher &#039;&#039;&#039;rechtzeitig ankündigen&#039;&#039;&#039; und mit einem &#039;&#039;&#039;Backup-Pin&#039;&#039;&#039; vorbereiten - der neue Pin muss vor dem Wechsel in einer Client-Version ausgerollt sein, sonst sperrt sich die App aus. Mit &#039;&#039;Eigenes Zertifikat (Pin)&#039;&#039; gibt OBS den Pin über den Zugangscode an die App weiter. Gepinnt wird der &#039;&#039;&#039;Schlüssel&#039;&#039;&#039;, nicht das Zertifikat: Erneuern Sie das Zertifikat mit demselben Schlüssel, bleibt der Pin gleich und kein Gerät braucht einen neuen Zugangscode.&lt;br /&gt;
==Automatische Zertifikatserneuerung==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server kann sein TLS-Zertifikat selbst bei einer Zertifizierungsstelle beantragen und vor Ablauf erneuern. Prüfarten, Voraussetzungen, Einrichtung, Ablauf und Konsolenbefehle beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]].&lt;br /&gt;
&lt;br /&gt;
===Felder===&lt;br /&gt;
&lt;br /&gt;
Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                       !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Automatische Erneuerung    || &#039;&#039;deaktiviert&#039;&#039; = aus. &#039;&#039;Produktivbetrieb&#039;&#039; = der Dienst beantragt und erneuert das Zertifikat selbst. &#039;&#039;Testbetrieb&#039;&#039; = nur für Testsysteme, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung#Testbetrieb|Testbetrieb]]&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikatsname (FQDN)     || Der Name, auf den das Zertifikat ausgestellt wird. Pflicht, sobald die Erneuerung aktiv ist&lt;br /&gt;
|-&lt;br /&gt;
| Kontakt bei der CA         || Optional. Die Zertifizierungsstelle schickt an diese Adresse Warnungen, wenn ein Zertifikat abzulaufen droht&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikat gültig bis      || Ablaufdatum des aktiven Zertifikats&lt;br /&gt;
|-&lt;br /&gt;
| Testzertifikat aus Staging || Gesetzt, wenn das aktive Zertifikat aus dem Testbetrieb stammt&lt;br /&gt;
|-&lt;br /&gt;
| Zustand                    || &#039;&#039;idle&#039;&#039; = nichts zu tun, &#039;&#039;pending&#039;&#039; = Vorgang läuft, &#039;&#039;error&#039;&#039; = letzter Versuch fehlgeschlagen&lt;br /&gt;
|-&lt;br /&gt;
| Letzte Meldung             || Ergebnis des letzten Vorgangs - im Störungsfall das erste Feld, in das Sie sehen&lt;br /&gt;
|-&lt;br /&gt;
| Gesperrt bis               || Nach einem Fehlschlag wartet der Dienst bis zu diesem Zeitpunkt, bevor er es erneut versucht&lt;br /&gt;
|-&lt;br /&gt;
| Letzter Selbsttest am      || Zeitpunkt der letzten Prüfung über das Testsystem der Zertifizierungsstelle&lt;br /&gt;
|-&lt;br /&gt;
| Selbsttest bestanden       || Ergebnis dieser Prüfung&lt;br /&gt;
|-&lt;br /&gt;
| Eigenes Zertifikat (Pin)   || Der Dienst stellt für dieses Profil ein eigenes Zertifikat aus, sobald Zertifikat und Schlüssel leer sind, und berechnet den zugehörigen Pin. Gedacht für interne Netze ohne eigene Domain, in denen keine Zertifizierungsstelle erreichbar ist. Nicht zusammen mit der automatischen Erneuerung&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikats-Pin            || Pin des Schlüssels, mit dem dieses Profil TLS spricht, in der Form &amp;lt;code&amp;gt;sha256/…&amp;lt;/code&amp;gt;. Der Dienst berechnet ihn; die App erhält ihn über den Zugangscode und prüft damit jede Verbindung&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die Felder von &#039;&#039;Zertifikat gültig bis&#039;&#039; bis &#039;&#039;Selbsttest bestanden&#039;&#039; sowie &#039;&#039;Zertifikats-Pin&#039;&#039; füllt der Dienst selbst; sie dienen der Anzeige.&lt;br /&gt;
&lt;br /&gt;
Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                       !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Prüfart                    || &#039;&#039;http-01 (Port 80)&#039;&#039; oder &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039;, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung#Welche Prüfart?|Welche Prüfart?]]&lt;br /&gt;
|-&lt;br /&gt;
| DNS-Anbieter               || Nur dns-01: &#039;&#039;acme-dns&#039;&#039;, &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Adresse (acme-dns)         || Nur acme-dns: Adresse der Instanz, beginnt mit &amp;lt;code&amp;gt;https://&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Benutzer (acme-dns)        || Nur acme-dns: Benutzer des Kontos. Bleiben Benutzer, Schlüssel und Name leer, legt der Dienst das Konto selbst an&lt;br /&gt;
|-&lt;br /&gt;
| Schlüssel / API-Token      || acme-dns: Passwort des Kontos. Hetzner und Cloudflare: API-Token. IONOS: API-Schlüssel in der Form &amp;lt;code&amp;gt;Präfix.Secret&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Name des TXT-Eintrags      || Leer = &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Zertifikatsname&amp;gt;&amp;lt;/code&amp;gt;. Bei CNAME-Delegation das Ziel des CNAME, bei acme-dns die &#039;&#039;fulldomain&#039;&#039; des Kontos&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Die Zugangsdaten stehen im Klartext im Server-Profil. Verwenden Sie Token, die nur die nötige Zone ändern dürfen.}}&lt;br /&gt;
&lt;br /&gt;
==Eigenes Zertifikat und Pin==&lt;br /&gt;
&lt;br /&gt;
In internen Netzen ohne eigene Domain ist keine Zertifizierungsstelle erreichbar, und ein von Hand erstelltes Zertifikat kennt keine App. Für diesen Fall setzen Sie den Haken &#039;&#039;&#039;Eigenes Zertifikat (Pin)&#039;&#039;&#039; und tragen unter &#039;&#039;&#039;Zertifikatsname (FQDN)&#039;&#039;&#039; die Adresse ein, unter der das Gerät den Server erreicht - auch eine IP-Adresse ist zulässig.&lt;br /&gt;
&lt;br /&gt;
Beim nächsten Start des REST-Dienstes stellt der Dienst ein Zertifikat aus, sofern &#039;&#039;&#039;Zertifikat (cert)&#039;&#039;&#039; und &#039;&#039;&#039;Schlüssel (key)&#039;&#039;&#039; leer sind, und berechnet den Pin. Ein bereits hinterlegtes Zertifikat bleibt unangetastet - der Dienst füllt nur, was leer ist.&lt;br /&gt;
&lt;br /&gt;
Den Pin gibt OBS zusammen mit Adresse und API-Key im Zugangscode an die App weiter. Die App vertraut dann genau diesem Schlüssel und keiner Zertifizierungsstelle; Name und Ablaufdatum des Zertifikats prüft sie nicht.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Ein Wechsel des Schlüssels macht &#039;&#039;&#039;jeden bisher ausgegebenen Zugangscode ungültig&#039;&#039;&#039; - die Geräte kommen dann nicht mehr durch. Wollen Sie den Schlüssel wechseln, geben Sie vorher Zugangscodes mit altem und neuem Pin aus und stellen erst um, wenn alle Geräte den neuen Code haben. Das Ausstellen wiederholt der Dienst von sich aus nie: Solange ein Zertifikat hinterlegt ist, bleibt es stehen.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Pin und die automatische Erneuerung schliessen sich aus. Jede Erneuerung erzeugt einen neuen Schlüssel und damit einen neuen Pin; die Geräte wären danach ausgesperrt. OBS weist die Kombination beim Speichern zurück.}}&lt;br /&gt;
&lt;br /&gt;
==Rate-Limit je Profil==&lt;br /&gt;
&lt;br /&gt;
Jedes Server-Profil führt &#039;&#039;&#039;eigene Zähler&#039;&#039;&#039; und kann &#039;&#039;&#039;eigene Schwellen&#039;&#039;&#039; haben.&lt;br /&gt;
Ein Profil, das gerade überrannt wird, bremst damit die anderen Profile nicht mit&lt;br /&gt;
aus - das Public-Profil und das interne mTLS-Profil sind voneinander unabhängig.&lt;br /&gt;
&lt;br /&gt;
Gezählt wird pro Schlüssel, und der Schlüssel ist der &#039;&#039;&#039;API-Key&#039;&#039;&#039; des Zugangs.&lt;br /&gt;
Sendet ein Konsument keinen API-Key (Zugang mit &#039;&#039;*&#039;&#039;), wird ersatzweise auf die&lt;br /&gt;
&#039;&#039;&#039;IP-Adresse&#039;&#039;&#039; gezählt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Das ist der wichtigste Punkt bei mobilen Anwendungen: Sitzen alle&lt;br /&gt;
Benutzer hinter einer gemeinsamen Firmen-IP und sendet die App keinen eigenen&lt;br /&gt;
API-Key, teilen sich &#039;&#039;&#039;alle Benutzer einen Zähler&#039;&#039;&#039;. Mit den Standardwerten sind&lt;br /&gt;
das 10 Fehlversuche für den ganzen Betrieb pro Minute - nach ein paar falschen&lt;br /&gt;
Passworteingaben steht die Abteilung. Für solche Profile die Schwellen bewusst&lt;br /&gt;
höher setzen oder jedem Konsumenten einen eigenen API-Key geben.}}&lt;br /&gt;
&lt;br /&gt;
Dasselbe gilt hinter einem Reverse-Proxy: Alle Anfragen kommen dort von der IP-Adresse&lt;br /&gt;
des Proxys, und alle Konsumenten ohne API-Key teilen sich einen Zähler. Der Header&lt;br /&gt;
&#039;&#039;X-Forwarded-For&#039;&#039; ändert daran nichts, er wird nur protokolliert.&lt;br /&gt;
&lt;br /&gt;
Bleiben alle vier Felder leer (bzw. 0), gelten die Standardwerte. Die Werte werden&lt;br /&gt;
&#039;&#039;&#039;beim Start&#039;&#039;&#039; des REST-Dienstes gelesen - eine Änderung wird erst nach einem&lt;br /&gt;
Neustart wirksam.&lt;br /&gt;
&lt;br /&gt;
==Validierung beim Speichern==&lt;br /&gt;
&lt;br /&gt;
Immer geprüft wird:&lt;br /&gt;
&lt;br /&gt;
* Nr und Name sind vergeben, Nr liegt zwischen 1 und 99.&lt;br /&gt;
* Nr und Name sind eindeutig.&lt;br /&gt;
* Eingetragene Zertifikate und Schlüssel liegen im PEM-Format vor.&lt;br /&gt;
* Ein eingetragener Zertifikatsname enthält keine Adresse, keinen Port und keine Leerzeichen.&lt;br /&gt;
* &#039;&#039;Eigenes Zertifikat (Pin)&#039;&#039; ist weder zusammen mit der automatischen Erneuerung noch zusammen mit &#039;&#039;kein SSL&#039;&#039; gesetzt.&lt;br /&gt;
&lt;br /&gt;
Nur bei &#039;&#039;&#039;aktivem&#039;&#039;&#039; Profil zusätzlich:&lt;br /&gt;
&lt;br /&gt;
* Ohne &#039;&#039;kein SSL&#039;&#039;, ohne automatische Erneuerung und ohne &#039;&#039;Eigenes Zertifikat (Pin)&#039;&#039; sind Zertifikat und Schlüssel vorhanden.&lt;br /&gt;
* Bei &#039;&#039;mTLS-Modus aktiv&#039;&#039; ist das Root-Zertifikat vorhanden.&lt;br /&gt;
* Bei aktiver Erneuerung ist der Zertifikatsname vergeben, und &#039;&#039;kein SSL&#039;&#039; ist nicht gesetzt.&lt;br /&gt;
* Bei &#039;&#039;Eigenes Zertifikat (Pin)&#039;&#039; ist der Zertifikatsname vergeben, solange kein Zertifikat hinterlegt ist - der Dienst stellt das Zertifikat auf diesen Namen aus.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein &#039;&#039;&#039;nicht aktives&#039;&#039;&#039; Profil dürfen Sie unvollständig speichern - es wird beim Start des Dienstes nicht geladen. Was Sie eintragen, wird trotzdem auf Plausibilität geprüft.}}&lt;br /&gt;
&lt;br /&gt;
==Bindungen==&lt;br /&gt;
&lt;br /&gt;
Eine Bindung ist die Kombination aus IP-Adresse und Port, auf der ein Server-Profil lauscht. Pro Server-Profil können mehrere Bindungen existieren, z.B. um die gleiche TLS-Konfiguration parallel auf 443 und 8443 anzubieten.&lt;br /&gt;
&lt;br /&gt;
===Aufruf===&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste den gewünschten Server markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die zum Server gehörende Bindungs-Liste.&lt;br /&gt;
&lt;br /&gt;
===Felder===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld    !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Host    || IP-Adresse oder Hostname, auf dem gelauscht wird. &#039;&#039;0.0.0.0&#039;&#039; = alle IPv4-Adressen; &#039;&#039;127.0.0.1&#039;&#039; = nur lokal; explizite IPs binden gezielt auf eine Netzwerkkarte&lt;br /&gt;
|-&lt;br /&gt;
| Port    || TCP-Port (1-65535). 443/8443 für HTTPS, frei wählbar für Debug-Profile&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv   || Bindung wird beim Server-Start geladen&lt;br /&gt;
|-&lt;br /&gt;
| Standard|| Markiert die Standard-Bindung des Servers. Pro Server-Profil ist nur eine Standard-Bindung erlaubt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Wirksamkeit===&lt;br /&gt;
&lt;br /&gt;
Bindungen werden ausschliesslich beim Start des REST-Dienstes geladen. Änderungen werden daher erst nach einem Neustart aktiv. Bis dahin laufen die alten Bindungen weiter.&lt;br /&gt;
&lt;br /&gt;
==Listen-Funktionen==&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfg&#039;&#039;&#039; - neues Server-Profil&lt;br /&gt;
* &#039;&#039;&#039;Return&#039;&#039;&#039; - vorhandenes Profil bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F4&#039;&#039;&#039; - Sortierung wählen&lt;br /&gt;
* &#039;&#039;&#039;F6&#039;&#039;&#039; - Bindungs-Liste des markierten Servers&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Endpunkte&amp;diff=67343</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Endpunkte</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Endpunkte&amp;diff=67343"/>
		<updated>2026-11-05T12:39:59Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Vorlage:-Praefix; Link ohne fuehrenden Schraegstrich korrigiert [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
= Endpunkte =&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Endpunkt&#039;&#039;&#039; stellt eine Adresse bereit, über die der REST-Server konkrete&lt;br /&gt;
Funktionen anbietet. Jeder Endpunkt wird durch ein OBS-Skript oder einen WebHook&lt;br /&gt;
realisiert und ist genau einem Server-Profil zugeordnet.&lt;br /&gt;
&lt;br /&gt;
== Routing über Pfad-Templates ==&lt;br /&gt;
&lt;br /&gt;
Das Routing erfolgt über die Spalte &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; (&amp;lt;code&amp;gt;re_pathtemplate&amp;lt;/code&amp;gt;).&lt;br /&gt;
Ein Template beschreibt den vollständigen Pfad nach dem Host und kann&lt;br /&gt;
&#039;&#039;&#039;Platzhalter&#039;&#039;&#039; enthalten:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Statische Segmente&#039;&#039;&#039; müssen exakt übereinstimmen (Groß-/Kleinschreibung wird ignoriert).&lt;br /&gt;
* &#039;&#039;&#039;Platzhalter&#039;&#039;&#039; in geschweiften Klammern – z.&amp;amp;nbsp;B. &amp;lt;code&amp;gt;{uid}&amp;lt;/code&amp;gt; – passen auf einen beliebigen Wert und werden als [[#Pfad-Parameter im Skript|Pfad-Parameter]] erfasst.&lt;br /&gt;
&lt;br /&gt;
Beispiele für gültige Templates:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Template !! Passt auf !! Pfad-Parameter&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/orders&amp;lt;/code&amp;gt; || –&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/orders/4711&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;uid = 4711&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders/{uid}/modules/{code}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/orders/4711/modules/A1&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;uid = 4711&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;code = A1&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/kalender/v1&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/kalender/v1&amp;lt;/code&amp;gt; || –&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Präzedenz bei mehreren Treffern ===&lt;br /&gt;
&lt;br /&gt;
Passen mehrere Templates auf denselben Pfad, &#039;&#039;&#039;gewinnt das spezifischste&#039;&#039;&#039; –&lt;br /&gt;
also das mit den meisten statischen Segmenten. So schlägt &amp;lt;code&amp;gt;/orders/summary&amp;lt;/code&amp;gt;&lt;br /&gt;
das Template &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt;, während &amp;lt;code&amp;gt;/orders/4711&amp;lt;/code&amp;gt; auf&lt;br /&gt;
&amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt; matcht.&lt;br /&gt;
&lt;br /&gt;
== Hauptfelder eines Endpunkts ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld !! Spalte !! Zweck&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_pathtemplate&amp;lt;/code&amp;gt; || &#039;&#039;&#039;Maßgeblich fürs Routing.&#039;&#039;&#039; Vollständiger Pfad mit Platzhaltern, z.&amp;amp;nbsp;B. &amp;lt;code&amp;gt;/orders/{uid}/modules/{code}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Server&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_server&amp;lt;/code&amp;gt; || Zuordnung zum Server-Profil (erforderlich)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Aktiv&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_aktiv&amp;lt;/code&amp;gt; || Statusflag; inaktive Endpunkte liefern 404&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Skript&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_script&amp;lt;/code&amp;gt; || DwScript-Quelltext des Handlers (siehe [[OBS/Kostenpflichtige_Module/RESTServer/Scripting|Scripting]])&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;WebHook&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_webhook&amp;lt;/code&amp;gt; || optional: Verweis auf einen Einmal-Endpunkt statt Skript&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Info&#039;&#039;&#039; || – || Dokumentations-Freitext&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_upload&amp;lt;/code&amp;gt; || Erlaubt Datei-Uploads an diesem Endpunkt (&amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt; = aus, &amp;lt;code&amp;gt;1&amp;lt;/code&amp;gt; = an). Ohne Freigabe werden Upload-Anfragen mit 415 abgelehnt.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Max. Upload-Grösse&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt; || Maximale Dateigrösse in MB. Leer/0 = Standard 25&amp;amp;nbsp;MB. Überschreitung wird mit 413 abgelehnt.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Pfad-Parameter im Skript ==&lt;br /&gt;
&lt;br /&gt;
Die aus den Platzhaltern erfassten Werte liest das Endpunkt-Skript über&lt;br /&gt;
&amp;lt;code&amp;gt;oReader.Path&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot;&amp;gt;&lt;br /&gt;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUid: string;&lt;br /&gt;
begin&lt;br /&gt;
    cUid := oReader.Path(&#039;uid&#039;);   // aus /orders/{uid}&lt;br /&gt;
    // ...&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Werte stammen aus dem Routing und können nicht durch den Client&lt;br /&gt;
überschrieben werden.&lt;br /&gt;
&lt;br /&gt;
== Datei-Uploads ==&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt nimmt Datei-Uploads nur entgegen, wenn er dafür freigeschaltet ist&lt;br /&gt;
(&amp;lt;code&amp;gt;re_upload = 1&amp;lt;/code&amp;gt;). Andernfalls werden Upload-Anfragen mit&lt;br /&gt;
&#039;&#039;&#039;415 Unsupported Media Type&#039;&#039;&#039; abgelehnt und das Skript wird nicht ausgeführt.&lt;br /&gt;
&lt;br /&gt;
Die maximal zulässige Dateigrösse wird pro Endpunkt über &amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;&lt;br /&gt;
(in MB) festgelegt; ohne Angabe gilt der Standard von &#039;&#039;&#039;25&amp;amp;nbsp;MB&#039;&#039;&#039;. Wird das&lt;br /&gt;
Limit überschritten, antwortet der Server mit &#039;&#039;&#039;413 Payload Too Large&#039;&#039;&#039;. Für&lt;br /&gt;
Uploads gilt nicht das JSON-Body-Limit (10&amp;amp;nbsp;MB), sondern dieses Endpunkt-Limit.&lt;br /&gt;
Der Originalname (beim resumable Upload über den Header &amp;lt;code&amp;gt;Upload-Metadata&amp;lt;/code&amp;gt;) ist &#039;&#039;&#039;Pflicht&#039;&#039;&#039;; fehlt er, wird der Upload mit &#039;&#039;&#039;400&#039;&#039;&#039; abgelehnt, ohne dass eine Datei angelegt wird. Das Skript liest Pfad, Name, Content-Type und die SHA-256-Prüfsumme der gespeicherten Datei über &amp;lt;code&amp;gt;oReader.UploadPath()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;UploadName()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;UploadType()&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;UploadSha()&amp;lt;/code&amp;gt;; bei &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; die übrigen Formularfelder über &amp;lt;code&amp;gt;oReader.Form(&#039;&amp;amp;lt;name&amp;amp;gt;&#039;)&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Das Übertragungsprotokoll&#039;&#039;&#039; - Header, Statuscodes, Resume-Verhalten - steht auf einer eigenen Seite: [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]].&lt;br /&gt;
&lt;br /&gt;
Unterstützt werden zwei Übertragungsarten:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfacher Upload&#039;&#039;&#039; per &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; (eine Datei pro Request).&lt;br /&gt;
* &#039;&#039;&#039;Resumable/Chunked Upload&#039;&#039;&#039; per &amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt; (grosse Dateien, fortsetzbar nach Abbruch).&lt;br /&gt;
&lt;br /&gt;
Der Server legt die hochgeladene Datei in einem temporären Verzeichnis ab und&lt;br /&gt;
übergibt dem Endpunkt-Skript den Pfad. Was mit der Datei geschieht (Ablage,&lt;br /&gt;
DMS-Verknüpfung, Weiterverarbeitung), entscheidet allein das Skript. Details und&lt;br /&gt;
Beispiele: [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
== Datei-Downloads ==&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt kann eine &#039;&#039;&#039;Datei&#039;&#039;&#039; statt eines JSON-Körpers zurückgeben -&lt;br /&gt;
PDF, CSV, eine APK. Dafür ist &#039;&#039;&#039;keine Freischaltung&#039;&#039;&#039; nötig: ein Download&lt;br /&gt;
entsteht dadurch, dass das Skript ihn mit &amp;lt;code&amp;gt;oWriter.SendFile(...)&amp;lt;/code&amp;gt;&lt;br /&gt;
bzw. &amp;lt;code&amp;gt;oWriter.SendTempFile(...)&amp;lt;/code&amp;gt; erzeugt, nicht dadurch, dass ein&lt;br /&gt;
Client ihn anfragt.&lt;br /&gt;
&lt;br /&gt;
Content-Type, Dateiname, Bereichsanfragen (&amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt;, &#039;&#039;&#039;206&#039;&#039;&#039;),&lt;br /&gt;
ETag und das Aufräumen temporärer Dateien übernimmt der Server. Einzelheiten:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]].&lt;br /&gt;
&lt;br /&gt;
== HTTP-Methode ==&lt;br /&gt;
&lt;br /&gt;
Welche Funktion aufgerufen wird, ergibt sich aus dem HTTP-Verb: Der Server ruft&lt;br /&gt;
die gleichnamige Skript-Methode auf (&amp;lt;code&amp;gt;Get&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Post&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Put&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Delete&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Patch&amp;lt;/code&amp;gt;).&lt;br /&gt;
Ein Template entspricht damit &#039;&#039;&#039;einem&#039;&#039;&#039; Endpunkt-Skript; unterschiedliche&lt;br /&gt;
Pfad-Formen (z.&amp;amp;nbsp;B. &amp;lt;code&amp;gt;/orders&amp;lt;/code&amp;gt; vs. &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt;) sind&lt;br /&gt;
&#039;&#039;&#039;eigene Endpunkt-Einträge&#039;&#039;&#039; mit eigenem Skript und eigener Berechtigung.&lt;br /&gt;
&lt;br /&gt;
== URL-Struktur ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
http://[Host][:Port][Pfad-Template]&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Beispiele:&lt;br /&gt;
* &amp;lt;code&amp;gt;https://api.meinserver.de/orders&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;https://api.meinserver.de/orders/4711&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;https://api.meinserver.de/orders/4711/modules/A1&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Versionierung ==&lt;br /&gt;
&lt;br /&gt;
Da es kein eigenes Versions-Feld mehr gibt, wird die Version als statisches&lt;br /&gt;
Segment ins Template aufgenommen, z.&amp;amp;nbsp;B. &amp;lt;code&amp;gt;/orders/v1&amp;lt;/code&amp;gt; oder&lt;br /&gt;
&amp;lt;code&amp;gt;/v1/orders/{uid}&amp;lt;/code&amp;gt;. Änderung ohne Breaking Change:&lt;br /&gt;
&lt;br /&gt;
# Bestehenden Endpunkt unverändert lassen.&lt;br /&gt;
# Neuen Endpunkt mit gleichem Ressourcennamen, aber neuem Versions-Segment im Template anlegen.&lt;br /&gt;
# Berechtigungen für neue Konsumenten setzen.&lt;br /&gt;
# Alten Endpunkt deaktivieren, wenn die Migration abgeschlossen ist.&lt;br /&gt;
&lt;br /&gt;
== Zugriffskontrolle ==&lt;br /&gt;
&lt;br /&gt;
Endpunkte sind Server-Profilen zugeordnet; ein Zugang benötigt eine explizite&lt;br /&gt;
Berechtigung (Tabelle &amp;lt;code&amp;gt;RESTSRV_ACCESS&amp;lt;/code&amp;gt;), um einen Endpunkt nutzen zu&lt;br /&gt;
dürfen.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Ein Endpunkt besteht aus zwei Zeilen, und beide werden von Hand&lt;br /&gt;
gepflegt:&#039;&#039;&#039; der Eintrag in &amp;lt;code&amp;gt;RESTSRV_ENDPOINTS&amp;lt;/code&amp;gt; und die Berechtigung&lt;br /&gt;
in &amp;lt;code&amp;gt;RESTSRV_ACCESS&amp;lt;/code&amp;gt;. Fehlt die zweite, antwortet der Endpunkt&lt;br /&gt;
&#039;&#039;&#039;403&#039;&#039;&#039; - und sieht dabei fertig aus: Pfad, Skript und Server-Profil stehen&lt;br /&gt;
korrekt da, der Aufruf kommt trotzdem nicht durch. Das ist die häufigste Ursache&lt;br /&gt;
für ein „der Endpunkt ist doch angelegt&amp;quot; und kostet jedes Mal eine&lt;br /&gt;
Fehlersuche, weil der Statuscode nach einem Rechteproblem des Konsumenten&lt;br /&gt;
aussieht und nicht nach einer fehlenden Konfigurationszeile.&lt;br /&gt;
&lt;br /&gt;
Beim Anlegen also &#039;&#039;&#039;immer beide Zeilen&#039;&#039;&#039;, und beim Prüfen eines&lt;br /&gt;
&amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; zuerst nachsehen, ob die zweite existiert.}} Weil jede Pfad-Form ein eigener Endpunkt-Eintrag ist, lässt sich der&lt;br /&gt;
Zugriff &#039;&#039;&#039;pro Pfad-Form&#039;&#039;&#039; granular vergeben (z.&amp;amp;nbsp;B. Lesen von&lt;br /&gt;
&amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt; erlauben, aber das ändernde&lt;br /&gt;
&amp;lt;code&amp;gt;PUT /orders/{uid}/modules/{code}&amp;lt;/code&amp;gt; nicht).&lt;br /&gt;
&lt;br /&gt;
== Caching ==&lt;br /&gt;
&lt;br /&gt;
Die Endpunkt-Definitionen werden pro Server-Profil zwischengespeichert&lt;br /&gt;
(TTL, Standard 60&amp;amp;nbsp;s). Neue oder geänderte Endpunkte/Templates werden daher&lt;br /&gt;
erst nach Ablauf des Caches (bzw. nach einer Cache-Invalidierung) wirksam – nicht&lt;br /&gt;
zwingend sofort.&lt;br /&gt;
&lt;br /&gt;
== HTTP-Fehlercodes ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Code !! Ursache&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;404&#039;&#039;&#039; || Kein Template passt zum Pfad, Endpunkt inaktiv oder falsches Server-Profil&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;403&#039;&#039;&#039; || Zugang fehlt in der Berechtigungsliste&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;401&#039;&#039;&#039; || API-Key ungültig/fehlend, JWT abgelaufen oder Sitzung gesperrt (&#039;&#039;AUTH_EXPIRED&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;500&#039;&#039;&#039; || Skript- oder Syntax-Fehler (Details in &amp;lt;code&amp;gt;RESTSRV_PROTO&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;405&#039;&#039;&#039; || Das Endpunkt-Skript hat für die angefragte HTTP-Methode keine Funktion; der Header &amp;lt;code&amp;gt;Allow&amp;lt;/code&amp;gt; nennt die vorhandenen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;206&#039;&#039;&#039; || Teilantwort eines Datei-Downloads auf eine &amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt;-Anfrage; die Antwort enthält &amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;416&#039;&#039;&#039; || Der angefragte &amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt; liegt hinter dem Dateiende; die Antwort nennt in &amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt; die wirkliche Grösse&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;415&#039;&#039;&#039; || Datei-Upload an einen Endpunkt, der dafür nicht freigeschaltet ist (&amp;lt;code&amp;gt;re_upload = 0&amp;lt;/code&amp;gt;); oder ein &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt;, das der Server nicht entpacken kann - unterstützt wird allein &amp;lt;code&amp;gt;gzip&amp;lt;/code&amp;gt; (siehe [[OBS/Kostenpflichtige Module/RESTServer|REST-Server]], Abschnitt Kompression)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;413&#039;&#039;&#039; || Hochgeladene Datei überschreitet die zulässige Maximalgrösse (&amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;); oder der Anfragekörper überschreitet 10 MB bzw. - gepackt gesendet - das 400fache der gesendeten Grösse&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;429&#039;&#039;&#039; || Rate-Limit überschritten; die Antwort enthält den Header &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;503&#039;&#039;&#039; || Vier Ursachen, unterscheidbar am &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt;: &amp;lt;code&amp;gt;IDEMPOTENCY_IN_PROGRESS&amp;lt;/code&amp;gt; (eine Anfrage mit demselben &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; wird gerade verarbeitet), &amp;lt;code&amp;gt;SERVICE_UNAVAILABLE&amp;lt;/code&amp;gt; bei erreichter Andrangsgrenze des Server-Profils (&amp;lt;code&amp;gt;rsv_max_parallel&amp;lt;/code&amp;gt;), bei einer Anmeldung, deren Sitzungszeile nicht geschrieben werden konnte, sowie bei einem fehlgeschlagenen &amp;lt;code&amp;gt;JwtRevoke&amp;lt;/code&amp;gt;. Alle vier tragen &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;409&#039;&#039;&#039; || Ein früherer Aufruf mit demselben &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; ist ohne Ergebnis geblieben (&amp;lt;code&amp;gt;IDEMPOTENCY_UNRESOLVED&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;422&#039;&#039;&#039; || Derselbe &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; wurde mit &#039;&#039;&#039;anderem&#039;&#039;&#039; Inhalt gesendet (&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_REUSED&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;400&#039;&#039;&#039; || Ein schreibender Aufruf (POST/PUT/PATCH/DELETE) &#039;&#039;&#039;ohne&#039;&#039;&#039; &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt;); ein resumable Upload ohne Dateinamen; oder eine Anfrage, die unverschlüsselt an einen TLS-Port geschickt wurde (&amp;lt;code&amp;gt;http&amp;lt;/code&amp;gt; statt &amp;lt;code&amp;gt;https&amp;lt;/code&amp;gt;) - letztere wird vor Authentifizierung und Endpunkt-Skript abgewiesen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;308&#039;&#039;&#039; || Zwischenantwort beim resumable Upload: das Teilstück wurde angehängt, der Upload ist noch nicht vollständig. Die Antwort trägt &amp;lt;code&amp;gt;Upload-Id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Upload-Offset&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt;; das Skript läuft dabei &#039;&#039;&#039;nicht&#039;&#039;&#039;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Jede Fehlerantwort besteht aus einem &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt;-Objekt mit&lt;br /&gt;
maschinenlesbarem Code - Aufbau siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer|Übersicht]].&lt;br /&gt;
&lt;br /&gt;
Daneben kann ein Endpunkt-Skript den Statuscode selbst setzen (z.B. &amp;lt;code&amp;gt;201&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;204&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;409&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;422&amp;lt;/code&amp;gt;) sowie Response-Header wie &amp;lt;code&amp;gt;ETag&amp;lt;/code&amp;gt; oder &amp;lt;code&amp;gt;Location&amp;lt;/code&amp;gt; - siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
== Idempotenz ==&lt;br /&gt;
&lt;br /&gt;
Bei &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;PUT&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;PATCH&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;DELETE&amp;lt;/code&amp;gt;&lt;br /&gt;
sorgt der Server selbst dafür, dass eine wiederholte Sendung &#039;&#039;&#039;keine&lt;br /&gt;
Zweitwirkung&#039;&#039;&#039; hat. Das gilt für &#039;&#039;&#039;jeden&#039;&#039;&#039; Endpunkt - es muss weder am Endpunkt&lt;br /&gt;
etwas eingestellt noch im Skript etwas programmiert werden.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Der Header &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; ist bei diesen vier Verben&lt;br /&gt;
Pflicht.&#039;&#039;&#039; Fehlt er, antwortet der Server mit &#039;&#039;&#039;400&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt; und das Endpunkt-Skript läuft &#039;&#039;&#039;nicht&#039;&#039;&#039; an.&lt;br /&gt;
&lt;br /&gt;
Bis v1.21 lief eine Anfrage ohne den Header ungeschützt durch; der Schutz gegen&lt;br /&gt;
die Doppelbuchung lag damit beim Client. Ausgenommen ist allein der&lt;br /&gt;
JWT-Endpunkt (Anmelden, Erneuern, Abmelden) - er greift vor der Adressauflösung&lt;br /&gt;
und kennt die Mechanik nicht.}}&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Situation !! Antwort des Servers&lt;br /&gt;
|-&lt;br /&gt;
| Kein &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; gesendet || &#039;&#039;&#039;400&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt;. Das Skript läuft nicht&lt;br /&gt;
|-&lt;br /&gt;
| Erster Aufruf || Endpunkt läuft normal, das Ergebnis wird zum Schlüssel gespeichert&lt;br /&gt;
|-&lt;br /&gt;
| Wiederholung, gleicher Inhalt || &#039;&#039;&#039;Gespeicherte Antwort&#039;&#039;&#039; (gleicher Statuscode, gleicher Body, gleiche Header) plus Header &amp;lt;code&amp;gt;Idempotent-Replay: true&amp;lt;/code&amp;gt;. Das Skript läuft &#039;&#039;&#039;nicht&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Wiederholung, anderer Inhalt || &#039;&#039;&#039;422&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_KEY_REUSED&amp;lt;/code&amp;gt; - derselbe Schlüssel für einen anderen Inhalt ist ein Client-Fehler&lt;br /&gt;
|-&lt;br /&gt;
| Erster Aufruf läuft noch || &#039;&#039;&#039;503&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_IN_PROGRESS&amp;lt;/code&amp;gt; mit &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;. Beim nächsten Versuch liegt die gespeicherte Antwort vor&lt;br /&gt;
|-&lt;br /&gt;
| Früherer Aufruf ohne Ergebnis || &#039;&#039;&#039;409&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_UNRESOLVED&amp;lt;/code&amp;gt;. Nur noch nach einem &#039;&#039;&#039;harten Abbruch&#039;&#039;&#039; des Dienstes zwischen Verarbeitung und Festschreiben; ob die Buchung stattgefunden hat, ist unbekannt. Der Aufräumlauf gibt solche Einträge nach 24 Stunden frei und protokolliert sie&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wichtig für die Auslegung eines Endpunkts:&lt;br /&gt;
&lt;br /&gt;
* Antwortet das Skript mit &#039;&#039;&#039;2xx&#039;&#039;&#039;, wird die Antwort gespeichert und bei einer Wiederholung erneut ausgeliefert.&lt;br /&gt;
* &#039;&#039;&#039;Ausnahme Dateiantwort:&#039;&#039;&#039; Liefert das Skript mit &amp;lt;code&amp;gt;SendFile&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;SendTempFile&amp;lt;/code&amp;gt; eine &#039;&#039;&#039;Datei&#039;&#039;&#039; aus, wird &#039;&#039;&#039;nichts&#039;&#039;&#039; festgeschrieben - eine Dateiantwort liesse sich nicht wiedergeben, gespeichert würde eine leere 200er-Antwort. Der Schlüssel wird stattdessen wieder freigegeben, ein erneuter Abruf derselben Datei ist unschädlich.&lt;br /&gt;
* Antwortet das Skript mit &#039;&#039;&#039;4xx&#039;&#039;&#039; (fachliche Ablehnung), wird der Schlüssel wieder &#039;&#039;&#039;freigegeben&#039;&#039;&#039; - der Konsument darf ihn nach Korrektur erneut verwenden. Sonst würde ein einziger Validierungsfehler den Schlüssel dauerhaft blockieren.&lt;br /&gt;
* Endet die Verarbeitung mit &#039;&#039;&#039;5xx&#039;&#039;&#039; oder einem Skript-Fehler, wird der Schlüssel &#039;&#039;&#039;freigegeben&#039;&#039;&#039; und der Fall protokolliert. Der Konsument darf denselben Schlüssel erneut senden.&lt;br /&gt;
* Das ist eine bewusste Abwägung (geändert 2026-08-25): Bleibt der Schlüssel nach einem Serverfehler belegt, ist der Vorgang &#039;&#039;&#039;unwiederholbar&#039;&#039;&#039; - der Client bekommt dauerhaft 503 und müsste die erfasste Arbeit verwerfen. Garantierter Datenverlust wiegt schwerer als ein möglicher Doppelsatz.&lt;br /&gt;
* &#039;&#039;&#039;Folge für die Auslegung:&#039;&#039;&#039; ein schreibender Endpunkt sollte selbst wiederholbar sein, etwa über ein fachliches Merkmal des Clients. Wo reines Anhängen stattfindet (Positionen, Dateien), bleibt sonst ein Restrisiko.&lt;br /&gt;
&lt;br /&gt;
Der Schlüssel gilt je &#039;&#039;&#039;Zugang, Methode und Pfad&#039;&#039;&#039;. Derselbe&lt;br /&gt;
&amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; an einem anderen Endpunkt ist damit ein eigener&lt;br /&gt;
Vorgang - der Konsument muss ihn nicht global eindeutig vergeben, aber pro Vorgang&lt;br /&gt;
&#039;&#039;&#039;stabil wiederverwenden&#039;&#039;&#039; (also nicht bei jedem Wiederholversuch neu erzeugen).&lt;br /&gt;
&lt;br /&gt;
Die Einträge stehen in &amp;lt;code&amp;gt;RESTSRV_IDEMPOTENCY&amp;lt;/code&amp;gt;. Abgeschlossene Einträge&lt;br /&gt;
werden nach &#039;&#039;&#039;30 Tagen&#039;&#039;&#039; gelöscht. Einträge &#039;&#039;&#039;ohne Ergebnis&#039;&#039;&#039; - sie entstehen&lt;br /&gt;
nur bei einem harten Abbruch des Dienstes - werden ab &#039;&#039;&#039;24 Stunden&#039;&#039;&#039; im Protokoll&lt;br /&gt;
gemeldet und &#039;&#039;&#039;anschliessend ebenfalls gelöscht&#039;&#039;&#039;. Das Stehenlassen wäre keine&lt;br /&gt;
Vorsicht, sondern eine Sperre: derselbe Vorgang mit demselben Schlüssel wäre sonst&lt;br /&gt;
nie wieder durchführbar.&lt;br /&gt;
&lt;br /&gt;
== WebHooks ==&lt;br /&gt;
&lt;br /&gt;
WebHooks sind &#039;&#039;&#039;Einmal-Endpunkte&#039;&#039;&#039; für asynchrone Callbacks. Beim ersten&lt;br /&gt;
erfolgreichen Aufruf wird die Antwort in der Tabelle &amp;lt;code&amp;gt;REMOTE_HOOK_URL&amp;lt;/code&amp;gt;&lt;br /&gt;
gespeichert und der Endpunkt anschließend automatisch gelöscht.&lt;br /&gt;
&lt;br /&gt;
Typischer Einsatz: OBS stösst einen Vorgang bei einem Fremdsystem an (Bezahldienst,&lt;br /&gt;
Versanddienstleister) und gibt diesem eine Rückruf-Adresse mit, die nur &#039;&#039;&#039;ein&lt;br /&gt;
einziges Mal&#039;&#039;&#039; gültig ist. Damit kann die Adresse nicht später erneut - oder von&lt;br /&gt;
jemand anderem - benutzt werden.&lt;br /&gt;
&lt;br /&gt;
Ablauf:&lt;br /&gt;
&lt;br /&gt;
# OBS legt einen Eintrag in &amp;lt;code&amp;gt;REMOTE_HOOK_URL&amp;lt;/code&amp;gt; an und dazu einen Endpunkt, dessen Feld &#039;&#039;&#039;WebHook&#039;&#039;&#039; (&amp;lt;code&amp;gt;re_webhook&amp;lt;/code&amp;gt;) auf diesen Eintrag zeigt. Ein Skript wird für diesen Endpunkt nicht gepflegt.&lt;br /&gt;
# Die Adresse dieses Endpunkts wird dem Fremdsystem als Callback-URL übergeben.&lt;br /&gt;
# Das Fremdsystem ruft die Adresse auf. Der Server legt den übergebenen Inhalt in &amp;lt;code&amp;gt;REMOTE_HOOK_URL.hu_response&amp;lt;/code&amp;gt; ab, antwortet mit &amp;lt;code&amp;gt;{&amp;quot;status&amp;quot;: &amp;quot;ok&amp;quot;}&amp;lt;/code&amp;gt; und &#039;&#039;&#039;löscht den Endpunkt&#039;&#039;&#039;.&lt;br /&gt;
# Der weiterverarbeitende OBS-Prozess findet die Rückmeldung in &amp;lt;code&amp;gt;hu_response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein zweiter Aufruf derselben Adresse läuft ins Leere: Der Endpunkt&lt;br /&gt;
existiert nicht mehr, die Antwort ist 404. Das ist gewollt - ein WebHook ist keine&lt;br /&gt;
dauerhafte Schnittstelle. Für eine dauerhaft erreichbare Rückmelde-Adresse einen&lt;br /&gt;
normalen Endpunkt mit Skript anlegen.}}&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel4&amp;diff=67344</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Beispiel4</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel4&amp;diff=67344"/>
		<updated>2026-11-05T12:39:58Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Vorlage:-Praefix; zwei Links ohne fuehrenden Schraegstrich korrigiert [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
= Beispiel 4 – Pfad-Parameter (REST-Routing über Templates) =&lt;br /&gt;
&lt;br /&gt;
Dieses Beispiel zeigt das &#039;&#039;&#039;Pfad-Template-Routing&#039;&#039;&#039; mit Platzhaltern. Eine&lt;br /&gt;
Auftrags-Ressource wird über drei Endpunkte abgebildet:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Methode + Pfad !! Zweck&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET /orders&amp;lt;/code&amp;gt; || Liste aller offenen Aufträge&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET /orders/{uid}&amp;lt;/code&amp;gt; || Einzelner Auftrag per UID&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;PUT /orders/{uid}/modules/{code}&amp;lt;/code&amp;gt; || Status eines Auftrags-Moduls ändern&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die Platzhalter &amp;lt;code&amp;gt;{uid}&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;{code}&amp;lt;/code&amp;gt; stehen im Skript als&lt;br /&gt;
&amp;lt;code&amp;gt;oReader.Path(&#039;uid&#039;)&amp;lt;/code&amp;gt; bzw. &amp;lt;code&amp;gt;oReader.Path(&#039;code&#039;)&amp;lt;/code&amp;gt; zur&lt;br /&gt;
Verfügung.&lt;br /&gt;
&lt;br /&gt;
== Einrichtung in OBS ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Server-Profil:&#039;&#039;&#039; Standard-TLS-Profil &#039;&#039;Public-API&#039;&#039; auf &amp;lt;code&amp;gt;0.0.0.0:443&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Zugang:&#039;&#039;&#039; &#039;&#039;Orders-Client&#039;&#039; mit API-Key, Zugriff auf die drei Endpunkte.&lt;br /&gt;
* &#039;&#039;&#039;Endpunkte&#039;&#039;&#039; (je ein Eintrag in &amp;lt;code&amp;gt;RESTSRV_ENDPOINTS&amp;lt;/code&amp;gt;, alle Profil &#039;&#039;Public-API&#039;&#039;):&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Endpunkt !! Pfad-Template !! Skript-Methode&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;orders&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/orders&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;Get&amp;lt;/code&amp;gt; (Liste)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;orders&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;Get&amp;lt;/code&amp;gt; (Einzel)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;orders&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/orders/{uid}/modules/{code}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;Put&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Endpunkt-Skript 1: &amp;lt;code&amp;gt;GET /orders&amp;lt;/code&amp;gt; (Liste) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot;&amp;gt;&lt;br /&gt;
// Hilfsfunktion: die FELDER eines Auftrags. Der Helfer bekommt den Writer und&lt;br /&gt;
// schreibt in das Objekt, das der Aufrufer schon geoeffnet hat - so laesst sich&lt;br /&gt;
// derselbe Auftrag einmal als Listeneintrag und einmal als ganze Antwort&lt;br /&gt;
// ausgeben, ohne den Code zu verdoppeln.&lt;br /&gt;
procedure _OrderFelder(oWriter: TxRestWriter; qOrder: TqSQL;&lt;br /&gt;
                       const cOffset: string);&lt;br /&gt;
begin&lt;br /&gt;
    oWriter.Str (&#039;uid&#039;   , qOrder.A2UID());&lt;br /&gt;
    oWriter.Str (&#039;nr&#039;    , qOrder.A2C(&#039;au_nr&#039;));&lt;br /&gt;
    oWriter.Str (&#039;kunde&#039; , qOrder.A2C(&#039;au_kunde&#039;));&lt;br /&gt;
    oWriter.Str (&#039;status&#039;, qOrder.A2C(&#039;au_status&#039;));&lt;br /&gt;
    // ISO-8601 mit Offset - siehe den Hinweis unter der Liste.&lt;br /&gt;
    oWriter.StrN(&#039;aenderung&#039;, RestIsoFromDate(qOrder.A2D(&#039;au_aend_dat&#039;), cOffset));&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cSql   : string;&lt;br /&gt;
    qData  : TqSQL;&lt;br /&gt;
    cOffset: string;&lt;br /&gt;
begin&lt;br /&gt;
    cOffset := oReader.Offset();&lt;br /&gt;
    // Die Antwort ist eine nackte Liste - RootArr vor dem ersten Eintrag.&lt;br /&gt;
    // Findet die Abfrage nichts, ist es eine leere Liste und kein leeres&lt;br /&gt;
    // Objekt: deshalb steht der Aufruf VOR der Abfrage.&lt;br /&gt;
    oWriter.RootArr();&lt;br /&gt;
&lt;br /&gt;
    cSql := &#039;SELECT * FROM auftraege&#039; +&lt;br /&gt;
            &#039; WHERE au_status &amp;lt;&amp;gt; &#039; + DB_SQLVal(&#039;9&#039;) +&lt;br /&gt;
            &#039; ORDER BY au_nr&#039;;&lt;br /&gt;
    if (DB_SOpen(oDB, cSql, qData)) then begin&lt;br /&gt;
        while (not qData.EoF) do begin&lt;br /&gt;
            oWriter.ObjBegin(&#039;&#039;);&lt;br /&gt;
                _OrderFelder(oWriter, qData, cOffset);&lt;br /&gt;
            oWriter.ObjEnd;&lt;br /&gt;
            qData.Next();&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
    DB_Close(qData);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Endpunkt-Skript 2: &amp;lt;code&amp;gt;GET /orders/{uid}&amp;lt;/code&amp;gt; (Einzel) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot;&amp;gt;&lt;br /&gt;
// Derselbe Helfer wie im Listen-Skript - hier schreibt er in die WURZEL, weil&lt;br /&gt;
// der Auftrag selbst die Antwort ist. Eine eigene Fehlerfunktion braucht es&lt;br /&gt;
// nicht mehr: oWriter.Error setzt Statuscode, code, message, uid und traceId.&lt;br /&gt;
// Derselbe Helfer wie im Listen-Skript - hier schreibt er in die WURZEL,&lt;br /&gt;
// weil der Auftrag selbst die Antwort ist.&lt;br /&gt;
procedure _OrderFelder(oWriter: TxRestWriter; qOrder: TqSQL;&lt;br /&gt;
                       const cOffset: string);&lt;br /&gt;
begin&lt;br /&gt;
    oWriter.Str (&#039;uid&#039;   , qOrder.A2UID());&lt;br /&gt;
    oWriter.Str (&#039;nr&#039;    , qOrder.A2C(&#039;au_nr&#039;));&lt;br /&gt;
    oWriter.Str (&#039;kunde&#039; , qOrder.A2C(&#039;au_kunde&#039;));&lt;br /&gt;
    oWriter.Str (&#039;status&#039;, qOrder.A2C(&#039;au_status&#039;));&lt;br /&gt;
    oWriter.StrN(&#039;aenderung&#039;, RestIsoFromDate(qOrder.A2D(&#039;au_aend_dat&#039;), cOffset));&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUid     : string;&lt;br /&gt;
    cSql     : string;&lt;br /&gt;
    qData    : TqSQL;&lt;br /&gt;
    lGefunden: Boolean;&lt;br /&gt;
    cOffset  : string;&lt;br /&gt;
begin&lt;br /&gt;
    // Pfad-Parameter aus /orders/{uid}&lt;br /&gt;
    cUid    := oReader.Path(&#039;uid&#039;);&lt;br /&gt;
    cOffset := oReader.Offset();&lt;br /&gt;
    if (cUid = &#039;&#039;) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Pfad-Parameter &amp;quot;uid&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    lGefunden := false;&lt;br /&gt;
    cSql := &#039;SELECT * FROM auftraege WHERE sys_uid = &#039; + DB_SQLVal(cUid);&lt;br /&gt;
    if (DB_SOpen(oDB, cSql, qData)) then begin&lt;br /&gt;
        if (not qData.EoF) then begin&lt;br /&gt;
            _OrderFelder(oWriter, qData, cOffset);&lt;br /&gt;
            lGefunden := true;&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
    DB_Close(qData);&lt;br /&gt;
&lt;br /&gt;
    if (not lGefunden) then begin&lt;br /&gt;
        oWriter.Error(404, &#039;NOT_FOUND&#039;, &#039;Auftrag nicht gefunden&#039;);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Endpunkt-Skript 3: &amp;lt;code&amp;gt;PUT /orders/{uid}/modules/{code}&amp;lt;/code&amp;gt; ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot;&amp;gt;&lt;br /&gt;
procedure Put(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUid   : string;&lt;br /&gt;
    cCode  : string;&lt;br /&gt;
    cStatus: string;&lt;br /&gt;
    cSql   : string;&lt;br /&gt;
    qChk   : TqSQL;&lt;br /&gt;
    xMod   : TqSQL;&lt;br /&gt;
    cModUid: string;&lt;br /&gt;
    lOk    : Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    // Beide Pfad-Parameter&lt;br /&gt;
    cUid  := oReader.Path(&#039;uid&#039;);&lt;br /&gt;
    cCode := oReader.Path(&#039;code&#039;);&lt;br /&gt;
    if (cUid = &#039;&#039;) or (cCode = &#039;&#039;) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Pfad-Parameter &amp;quot;uid&amp;quot; oder &amp;quot;code&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // Nutzdaten kommen aus dem JSON-Body. Der Rueckgabewert sagt, ob das Feld&lt;br /&gt;
    // ueberhaupt gesendet wurde - eine eigene Hilfsfunktion dafuer braucht es&lt;br /&gt;
    // nicht mehr.&lt;br /&gt;
    cStatus := &#039;&#039;;&lt;br /&gt;
    oReader.Str(&#039;status&#039;, cStatus);&lt;br /&gt;
    if (cStatus = &#039;&#039;) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Feld &amp;quot;status&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // Existenzpruefung - und zugleich der Schreib-Index. Ein SELECT auf die&lt;br /&gt;
    // sys_uid liefert beides; ein &#039;SELECT *&#039; waere hier verschenkte Arbeit.&lt;br /&gt;
    cSql := &#039;SELECT sys_uid FROM auftrag_module&#039; +&lt;br /&gt;
            &#039; WHERE am_auftrag = &#039; + DB_SQLVal(cUid) +&lt;br /&gt;
            &#039; AND am_code = &#039; + DB_SQLVal(cCode) + &#039; LIMIT 1&#039;;&lt;br /&gt;
    cModUid := &#039;&#039;;&lt;br /&gt;
    if (DB_SOpen(oDB, cSql, qChk)) then begin&lt;br /&gt;
        if (not qChk.EoF) then begin&lt;br /&gt;
            cModUid := qChk.A2UID();&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
    DB_Close(qChk);&lt;br /&gt;
&lt;br /&gt;
    if (cModUid = &#039;&#039;) then begin&lt;br /&gt;
        oWriter.Error(404, &#039;NOT_FOUND&#039;, &#039;Modul nicht gefunden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // Update&lt;br /&gt;
    // Der Satz wird ueber seine sys_uid geschrieben - qSqlRead waere hier&lt;br /&gt;
    // ein zusaetzlicher Lesezugriff, und ein unveraenderter Wert wuerde gar&lt;br /&gt;
    // kein Write erzeugen (SaveData liefert dann False).&lt;br /&gt;
    xMod := qSqlInit(oDB, &#039;auftrag_module&#039;);&lt;br /&gt;
    xMod.qSet(&#039;sys_uid&#039;  , cModUid);&lt;br /&gt;
    xMod.qSet(&#039;am_status&#039;, cStatus);&lt;br /&gt;
    // Rueckgabewert auswerten - ein Schreibfehler wirft keine Exception&lt;br /&gt;
    lOk := xMod.SaveData(UPDATE_RECORD);&lt;br /&gt;
    qSqlFree(xMod);&lt;br /&gt;
&lt;br /&gt;
    if (not lOk) then begin&lt;br /&gt;
        oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Status konnte nicht gespeichert werden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    oWriter.Str(&#039;status&#039;, &#039;ok&#039;);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Test mit curl ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Liste&lt;br /&gt;
curl -H &amp;quot;apikey: GEHEIM123&amp;quot; https://api.meinserver.de/orders&lt;br /&gt;
&lt;br /&gt;
# Einzelner Auftrag (uid als Pfad-Parameter)&lt;br /&gt;
curl -H &amp;quot;apikey: GEHEIM123&amp;quot; https://api.meinserver.de/orders/4711&lt;br /&gt;
&lt;br /&gt;
# Modul-Status aendern (uid + code als Pfad-Parameter, status im Body)&lt;br /&gt;
curl -X PUT \&lt;br /&gt;
     -H &amp;quot;apikey: GEHEIM123&amp;quot; \&lt;br /&gt;
     -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
     -d &#039;{&amp;quot;status&amp;quot;:&amp;quot;21&amp;quot;}&#039; \&lt;br /&gt;
     https://api.meinserver.de/orders/4711/modules/A1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Was dieses Beispiel zeigt ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Eine Ressource, mehrere Pfad-Formen&#039;&#039;&#039; über getrennte Endpunkt-Einträge mit eigenem Skript und eigener Berechtigung.&lt;br /&gt;
* &#039;&#039;&#039;Pfad-Parameter&#039;&#039;&#039; werden über &amp;lt;code&amp;gt;{name}&amp;lt;/code&amp;gt; im Template erfasst und im Skript mit &amp;lt;code&amp;gt;oReader.Path(&#039;name&#039;)&amp;lt;/code&amp;gt; gelesen.&lt;br /&gt;
* Reihenfolge der Skript-Parameter: &#039;&#039;&#039;&amp;lt;code&amp;gt;oReader&amp;lt;/code&amp;gt; zuerst, &amp;lt;code&amp;gt;oWriter&amp;lt;/code&amp;gt; danach&#039;&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;Ein Helfer bekommt den Writer&#039;&#039;&#039; und schreibt an der Stelle hinein, an der er gerufen wird. Derselbe &amp;lt;code&amp;gt;_OrderFelder&amp;lt;/code&amp;gt; bedient den Listeneintrag und die Einzelantwort - einmal in einem &amp;lt;code&amp;gt;ObjBegin(&#039;&#039;)&amp;lt;/code&amp;gt;, einmal in der Wurzel.&lt;br /&gt;
* Präzedenz: ein statisches Segment (z.&amp;amp;nbsp;B. &amp;lt;code&amp;gt;/orders/summary&amp;lt;/code&amp;gt;) hätte Vorrang vor &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Siehe auch ==&lt;br /&gt;
&lt;br /&gt;
* [[OBS/Kostenpflichtige_Module/RESTServer/Endpunkte|Endpunkte]]&lt;br /&gt;
* [[OBS/Kostenpflichtige_Module/RESTServer/Scripting|Scripting]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel5&amp;diff=67346</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Beispiel5</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel5&amp;diff=67346"/>
		<updated>2026-11-05T12:39:57Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Navigationsvorlage mit Vorlage:-Praefix [Volltext ersetzt] [Styleguide-Abweichung: Seitenkopf der Altseite]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz=&lt;br /&gt;
&lt;br /&gt;
Dieses Beispiel zeigt einen schreibenden Endpunkt für eine mobile App: Aufträge werden mit echten HTTP-Statuscodes aktualisiert, konkurrierende Änderungen über ETag/If-Match abgesichert (Optimistic Concurrency) und doppelte Sendungen über einen Idempotency-Key abgefangen. Die Anmeldung nutzt JWT mit Custom-Claims (Mandant, Rollen) und einem Refresh-Token.&lt;br /&gt;
&lt;br /&gt;
==Einrichtung in OBS==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Server-Profil:&#039;&#039;&#039; Standard-TLS-Profil &#039;&#039;Public-API&#039;&#039; (Port 443)&lt;br /&gt;
* &#039;&#039;&#039;Zugang:&#039;&#039;&#039; &#039;&#039;Mobile-App&#039;&#039;&lt;br /&gt;
** API-Key: zufällig generiert&lt;br /&gt;
** JWT aktiv, JWT-Endpunkt &#039;&#039;auth&#039;&#039;, JWT-Key zufällig, JWT-Exp 60 (Minuten)&lt;br /&gt;
* &#039;&#039;&#039;Endpunkte&#039;&#039;&#039; (beide dem Profil &#039;&#039;Public-API&#039;&#039; zugeordnet):&lt;br /&gt;
** &#039;&#039;orders/{uid}&#039;&#039; - Auftrag lesen/Ändern&lt;br /&gt;
** &#039;&#039;orders/{uid}/material&#039;&#039; - Material erfassen&lt;br /&gt;
* &#039;&#039;&#039;Berechtigung:&#039;&#039;&#039; Zugang &#039;&#039;Mobile-App&#039;&#039; für beide Endpunkte freigeschaltet&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Für das Abmelden gibt es &#039;&#039;&#039;keinen eigenen Endpunkt&#039;&#039;&#039;. Es läuft über&lt;br /&gt;
ein &#039;&#039;DELETE&#039;&#039; auf den JWT-Endpunkt &#039;&#039;auth&#039;&#039; und braucht daher weder eine Zeile&lt;br /&gt;
in &#039;&#039;RESTSRV_ENDPOINTS&#039;&#039; noch eine Berechtigung.}}&lt;br /&gt;
&lt;br /&gt;
==JWT-Authentifizierungs-Skript (Zugang)==&lt;br /&gt;
&lt;br /&gt;
Stellt Mandant und Rollen als Custom-Claims aus und löst über &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039; ein Refresh-Token aus. Bei einem Refresh ruft der Server im selben Skript die Methode &#039;&#039;Refresh&#039;&#039; auf. Die Hilfsfunktionen (&#039;&#039;PasswortPasst&#039;&#039;, &#039;&#039;TechnikerMandant&#039;&#039;, &#039;&#039;TechnikerRollen&#039;&#039;, &#039;&#039;TechnikerAktiv&#039;&#039;, &#039;&#039;AuftragLesen&#039;&#039;, &#039;&#039;AuftragVersion&#039;&#039;, &#039;&#039;AuftragSpeichern&#039;&#039;, &#039;&#039;MaterialAnlegen&#039;&#039;, &#039;&#039;ArtikelPreis&#039;&#039;, &#039;&#039;PushRegistrierungLoeschen&#039;&#039;) sind illustrativ und projektabhängig - sie stehen für Ihre Fachlogik, nicht für eine Server-API.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Eine &#039;&#039;&#039;eigene Sperrtabelle für Refresh-jti ist nicht nötig&#039;&#039;&#039;.&lt;br /&gt;
Einmalgebrauch, Rotation und Widerruf führt der Server in &#039;&#039;RESTSRV_TOKEN&#039;&#039; -&lt;br /&gt;
das Skript entscheidet nur, &#039;&#039;&#039;ob&#039;&#039;&#039; und &#039;&#039;&#039;mit welchen Rechten&#039;&#039;&#039; die Sitzung&lt;br /&gt;
fortgesetzt wird.}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Authenticate(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUser: string;&lt;br /&gt;
    cPass: string;&lt;br /&gt;
begin&lt;br /&gt;
    cUser := &#039;&#039;;&lt;br /&gt;
    cPass := &#039;&#039;;&lt;br /&gt;
    oReader.Str(&#039;username&#039;, cUser);&lt;br /&gt;
    oReader.Str(&#039;password&#039;, cPass);&lt;br /&gt;
&lt;br /&gt;
    if (PasswortPasst(cUser, cPass)) then begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;               , 1);&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_ID&#039;          , cUser);&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_SUBJECT&#039;     , cUser);&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_CLAIM_tenant&#039;, TechnikerMandant(cUser));&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_CLAIM_roles&#039; , TechnikerRollen(cUser));   // z.B. &amp;quot;tech,lead&amp;quot;&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_REFRESH_ID&#039;  , GlobalUID());   // loest das Refresh-Token aus&lt;br /&gt;
        // _OBS_JWT_REFRESH_EXP weggelassen -&amp;gt; Default 90 Tage&lt;br /&gt;
    end else begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;, 9);&lt;br /&gt;
        oWriter.Str(&#039;error&#039; , &#039;Login fehlgeschlagen&#039;);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure Refresh(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUser: string;&lt;br /&gt;
begin&lt;br /&gt;
    cUser := oReader.Subject();&lt;br /&gt;
&lt;br /&gt;
    // Das vorgelegte Refresh-Token hat der Server bereits geprueft und&lt;br /&gt;
    // entwertet. Hier wird nur entschieden, ob die Sitzung fortgesetzt&lt;br /&gt;
    // werden darf - und mit welchen Rechten.&lt;br /&gt;
    if (not TechnikerAktiv(cUser)) then begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;, 9);&lt;br /&gt;
        oWriter.Str(&#039;error&#039; , &#039;Konto ist nicht mehr aktiv, bitte neu anmelden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // Mandant und Rollen NEU lesen, nicht aus dem alten Token uebernehmen -&lt;br /&gt;
    // sonst wirkt ein Rechteentzug erst beim naechsten Login.&lt;br /&gt;
    oWriter.Int(&#039;status&#039;               , 1);&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_ID&#039;          , cUser);&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_SUBJECT&#039;     , cUser);&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_CLAIM_tenant&#039;, TechnikerMandant(cUser));&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_CLAIM_roles&#039; , TechnikerRollen(cUser));&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_REFRESH_ID&#039;  , GlobalUID());&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Endpunkt &#039;&#039;orders/{uid}&#039;&#039; - ändern mit ETag / If-Match==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;GET&#039;&#039; liefert den Auftrag samt &#039;&#039;ETag&#039;&#039; (Version), &#039;&#039;PUT&#039;&#039; prüft Rolle und &#039;&#039;If-Match&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
// AuftragLesen bekommt den WRITER und schreibt die Felder des Auftrags in die&lt;br /&gt;
// Wurzel; nVer kommt als var-Parameter zurueck. Vorher wurde ihm das&lt;br /&gt;
// Antwortobjekt hineingegeben - derselbe Gedanke, nur ohne Objekt.&lt;br /&gt;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUid: string;&lt;br /&gt;
    nVer: Integer;&lt;br /&gt;
begin&lt;br /&gt;
    cUid := oReader.Path(&#039;uid&#039;);&lt;br /&gt;
    if (not AuftragLesen(oWriter, oReader.Claim(&#039;tenant&#039;), cUid, nVer)) then begin&lt;br /&gt;
        oWriter.Error(404, &#039;NOT_FOUND&#039;, &#039;Auftrag nicht gefunden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
    oWriter.Header(&#039;ETag&#039;, xStr(nVer));&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure Put(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUid    : string;&lt;br /&gt;
    nAktuell: Integer;&lt;br /&gt;
    nIfMatch: Integer;&lt;br /&gt;
begin&lt;br /&gt;
    // nur Rolle &amp;quot;lead&amp;quot; darf ändern - Rolle kommt aus dem Token, nicht aus dem Body.&lt;br /&gt;
    // Mit Trennzeichen suchen: ein blosses Pos(&#039;lead&#039;, ...) wuerde auch in&lt;br /&gt;
    // &amp;quot;leadless&amp;quot; oder &amp;quot;teamlead&amp;quot; treffen.&lt;br /&gt;
    if (Pos(&#039;,lead,&#039;, &#039;,&#039; + oReader.Claim(&#039;roles&#039;) + &#039;,&#039;) = 0) then begin&lt;br /&gt;
        oWriter.Error(403, &#039;FORBIDDEN_ROLE&#039;, &#039;Diese Rolle darf nicht aendern&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    cUid     := oReader.Path(&#039;uid&#039;);&lt;br /&gt;
    nAktuell := AuftragVersion(cUid);&lt;br /&gt;
    nIfMatch := iVal(oReader.Param(&#039;if-match&#039;));&lt;br /&gt;
&lt;br /&gt;
    if (nIfMatch &amp;lt;&amp;gt; nAktuell) then begin&lt;br /&gt;
        // Der ETag gehoert AUCH an die Ablehnung - sonst kennt der Client den&lt;br /&gt;
        // gueltigen Wert nicht und sein naechster Versuch scheitert wieder.&lt;br /&gt;
        // Header und Error stoeren sich nicht: Error ersetzt nur den Koerper.&lt;br /&gt;
        oWriter.Header(&#039;ETag&#039;, xStr(nAktuell));&lt;br /&gt;
        oWriter.Error(409, &#039;VERSION_CONFLICT&#039;, &#039;Der Auftrag wurde zwischenzeitlich geaendert&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // AuftragSpeichern gibt den Rueckgabewert von SaveData durch. Ohne&lt;br /&gt;
    // diese Pruefung antwortet der Endpunkt mit 200 und einem neuen ETag,&lt;br /&gt;
    // obwohl nichts geschrieben wurde - siehe Scripting, Schreibfehler&lt;br /&gt;
    // erkennen.&lt;br /&gt;
    if (not AuftragSpeichern(cUid, oReader)) then begin   // setzt Version auf nAktuell + 1&lt;br /&gt;
        oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Der Auftrag konnte nicht gespeichert werden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    oWriter.Header(&#039;ETag&#039;, xStr(nAktuell + 1));&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Endpunkt &#039;&#039;orders/{uid}/material&#039;&#039; - idempotentes Anlegen==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;POST&#039;&#039; erfasst &#039;&#039;&#039;mehrere&#039;&#039;&#039; Materialpositionen in einem Aufruf: Validierungsfehler -&amp;gt; 422, erfolgreiches Anlegen -&amp;gt; 201 mit den vergebenen Positionen.&lt;br /&gt;
&lt;br /&gt;
Gegen doppelte Sendungen schickt der Client den Header &#039;&#039;Idempotency-Key&#039;&#039;. &#039;&#039;&#039;Das&lt;br /&gt;
Skript muss dafür nichts tun&#039;&#039;&#039; - der Server erkennt den Header, merkt sich das&lt;br /&gt;
Ergebnis und liefert bei einer Wiederholung mit demselben Schlüssel die&lt;br /&gt;
gespeicherte Antwort zurück, ohne das Skript erneut zu starten (siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]], Abschnitt&lt;br /&gt;
Idempotenz). Das Skript kümmert sich nur um seine Fachlogik:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
// Koerper:  {&amp;quot;positionen&amp;quot;:[{&amp;quot;artikel&amp;quot;:&amp;quot;4711&amp;quot;,&amp;quot;menge&amp;quot;:2},&lt;br /&gt;
//                          {&amp;quot;artikel&amp;quot;:&amp;quot;0815&amp;quot;,&amp;quot;menge&amp;quot;:1}]}&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUid     : string;&lt;br /&gt;
    cPos     : string;&lt;br /&gt;
    cArtikel : string;&lt;br /&gt;
    cNeueUid : string;&lt;br /&gt;
    nMenge   : Integer;&lt;br /&gt;
    nPreis   : Double;&lt;br /&gt;
    nGesamt  : Double;&lt;br /&gt;
    nAnz     : Integer;&lt;br /&gt;
    nI       : Integer;&lt;br /&gt;
begin&lt;br /&gt;
    cUid := oReader.Path(&#039;uid&#039;);&lt;br /&gt;
&lt;br /&gt;
    // 1) Gestalt pruefen. Count liefert 0, wenn das Feld fehlt ODER keine&lt;br /&gt;
    //    Liste ist - IsArr unterscheidet die beiden Faelle. Ein Objekt, wo&lt;br /&gt;
    //    eine Liste erwartet wird, ist etwas anderes als eine leere Liste.&lt;br /&gt;
    nAnz := oReader.Count(&#039;positionen&#039;);&lt;br /&gt;
    if (not oReader.IsArr(&#039;positionen&#039;)) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;positionen muss eine Liste sein&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
    if (nAnz = 0) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;positionen ist leer&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // 2) ALLE Positionen pruefen, BEVOR die erste gebucht wird. Sonst&lt;br /&gt;
    //    haette ein Fehler in Position 3 zwei gebuchte Zeilen und eine&lt;br /&gt;
    //    Fehlerantwort hinterlassen - der Client wiederholt und bucht die&lt;br /&gt;
    //    ersten zwei erneut.&lt;br /&gt;
    //&lt;br /&gt;
    //    Verschachtelung steht im Pfad: Punkt fuer Felder, eckige Klammern&lt;br /&gt;
    //    fuer Listeneintraege.&lt;br /&gt;
    for nI := 0 to nAnz - 1 do begin&lt;br /&gt;
        cPos     := &#039;positionen[&#039; + IntToStr(nI) + &#039;]&#039;;&lt;br /&gt;
        cArtikel := &#039;&#039;;&lt;br /&gt;
        nMenge   := 0;&lt;br /&gt;
        oReader.Str(cPos + &#039;.artikel&#039;, cArtikel);&lt;br /&gt;
        oReader.Int(cPos + &#039;.menge&#039;  , nMenge);&lt;br /&gt;
        if ((Empty(cArtikel)) or (nMenge &amp;lt;= 0)) then begin&lt;br /&gt;
            oWriter.Error(422, &#039;VALIDATION_FAILED&#039;,&lt;br /&gt;
                          &#039;Position &#039; + IntToStr(nI + 1) + &#039;: artikel und menge sind Pflicht&#039;);&lt;br /&gt;
            exit;&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // 3) Buchen und antworten in EINEM Durchgang. Die Positionsnummern&lt;br /&gt;
    //    entstehen beim Buchen, also schreibt die Schleife sie an der Stelle,&lt;br /&gt;
    //    an die sie gehoeren - gesammelt und am Ende eingesetzt werden muss&lt;br /&gt;
    //    nichts.&lt;br /&gt;
    nGesamt := 0;&lt;br /&gt;
    oWriter.ArrBegin(&#039;positionen&#039;);&lt;br /&gt;
    for nI := 0 to nAnz - 1 do begin&lt;br /&gt;
        cPos     := &#039;positionen[&#039; + IntToStr(nI) + &#039;]&#039;;&lt;br /&gt;
        cArtikel := &#039;&#039;;&lt;br /&gt;
        nMenge   := 0;&lt;br /&gt;
        oReader.Str(cPos + &#039;.artikel&#039;, cArtikel);&lt;br /&gt;
        oReader.Int(cPos + &#039;.menge&#039;  , nMenge);&lt;br /&gt;
&lt;br /&gt;
        cNeueUid := MaterialAnlegen(cUid, cArtikel, nMenge);&lt;br /&gt;
        if (Empty(cNeueUid)) then begin&lt;br /&gt;
            // Der Stapel wird GESCHLOSSEN, bevor der Fehler gesetzt wird:&lt;br /&gt;
            // oWriter.Error verwirft zwar den begonnenen Koerper, aber ein&lt;br /&gt;
            // offener ArrBegin bleibt ein Strukturfehler - dann antwortet der&lt;br /&gt;
            // Server 500 mit &amp;quot;Verschachtelung nicht geschlossen&amp;quot; statt mit&lt;br /&gt;
            // dieser Meldung.&lt;br /&gt;
            oWriter.ArrEnd;&lt;br /&gt;
            oWriter.Error(500, &#039;INTERNAL_ERROR&#039;,&lt;br /&gt;
                          &#039;Position &#039; + IntToStr(nI + 1) + &#039; konnte nicht gebucht werden&#039;);&lt;br /&gt;
            exit;&lt;br /&gt;
        end;&lt;br /&gt;
&lt;br /&gt;
        nPreis  := ArtikelPreis(cArtikel) * nMenge;&lt;br /&gt;
        nGesamt := nGesamt + nPreis;&lt;br /&gt;
&lt;br /&gt;
        oWriter.ObjBegin(&#039;&#039;);            // leerer Name = Listeneintrag&lt;br /&gt;
            oWriter.Str(&#039;uid&#039;    , cNeueUid);&lt;br /&gt;
            oWriter.Str(&#039;artikel&#039;, cArtikel);&lt;br /&gt;
            oWriter.Int(&#039;menge&#039;  , nMenge);&lt;br /&gt;
            // Zwei Nachkommastellen, immer mit Dezimalpunkt. Selbst&lt;br /&gt;
            // formatiert stand bei Werten ab 1000 der Tausendertrenner im&lt;br /&gt;
            // JSON - gueltiges JSON, falscher Wert, keine Meldung.&lt;br /&gt;
            oWriter.Num(&#039;preis&#039;  , nPreis, 2);&lt;br /&gt;
        oWriter.ObjEnd;&lt;br /&gt;
    end;&lt;br /&gt;
    oWriter.ArrEnd;&lt;br /&gt;
&lt;br /&gt;
    oWriter.Num(&#039;gesamt&#039;, nGesamt, 2);&lt;br /&gt;
    oWriter.Status(201);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Abmelden - ohne Endpunkt und ohne Skript==&lt;br /&gt;
&lt;br /&gt;
Beim Abmelden soll das Gerät seine Sitzung wirklich verlieren, nicht erst mit dem&lt;br /&gt;
Ablauf des Tokens. Dafür ist &#039;&#039;&#039;nichts einzurichten&#039;&#039;&#039;: ein &#039;&#039;DELETE&#039;&#039; auf den&lt;br /&gt;
JWT-Endpunkt mit dem Access-Token genügt, der Server sperrt die Sitzung in&lt;br /&gt;
&#039;&#039;RESTSRV_TOKEN&#039;&#039; und antwortet 204.&lt;br /&gt;
&lt;br /&gt;
Gesperrt wird die ganze Sitzung, also auch die Access-Token vorheriger&lt;br /&gt;
Erneuerungen. Ein zweiter Aufruf ist unschädlich und bleibt 204. Kann der Server&lt;br /&gt;
die Sitzung nicht sperren, antwortet er &#039;&#039;&#039;503&#039;&#039;&#039; mit &#039;&#039;Retry-After&#039;&#039; - die App&lt;br /&gt;
wiederholt den Abmeldevorgang dann.&lt;br /&gt;
&lt;br /&gt;
Nur wenn beim Abmelden zusätzlich etwas passieren soll - hier: die&lt;br /&gt;
Geräteregistrierung für Push löschen -, braucht es einen eigenen Endpunkt. Er&lt;br /&gt;
erledigt seine Fachlogik und ruft &#039;&#039;oWriter.JwtRevoke&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cAlle : string;&lt;br /&gt;
    lAlle : Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    PushRegistrierungLoeschen(oReader.Claim(&#039;technikerUid&#039;));&lt;br /&gt;
&lt;br /&gt;
    // Optional: {&amp;quot;alleGeraete&amp;quot;: true} meldet den Techniker ueberall ab.&lt;br /&gt;
    // Diese Entscheidung gehoert bewusst in ein Skript - sie braucht eine&lt;br /&gt;
    // Berechtigungspruefung, die der JWT-Endpunkt nicht leisten kann.&lt;br /&gt;
    lAlle := false;&lt;br /&gt;
    oReader.Bool(&#039;alleGeraete&#039;, lAlle);&lt;br /&gt;
    cAlle := IIF(lAlle, &#039;all&#039;, &#039;session&#039;);&lt;br /&gt;
&lt;br /&gt;
    oWriter.JwtRevoke(cAlle);&lt;br /&gt;
    oWriter.Status(204);&lt;br /&gt;
    oWriter.NoBody();&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Test mit curl==&lt;br /&gt;
&lt;br /&gt;
Token holen:&lt;br /&gt;
&lt;br /&gt;
 curl -X POST -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Content-Type: application/json&amp;quot; ^&lt;br /&gt;
      -d &amp;quot;{\&amp;quot;username\&amp;quot;:\&amp;quot;tech1\&amp;quot;,\&amp;quot;password\&amp;quot;:\&amp;quot;geheim\&amp;quot;}&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/auth/&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;token&amp;quot;:&amp;quot;eyJ...&amp;quot;,&amp;quot;accessToken&amp;quot;:&amp;quot;eyJ...&amp;quot;,&amp;quot;refreshToken&amp;quot;:&amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;:3600,&amp;quot;serverTime&amp;quot;:&amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Bei falschen Zugangsdaten antwortet der Server mit &#039;&#039;&#039;401&#039;&#039;&#039; und dem Text aus dem&lt;br /&gt;
Authenticate-Skript als &#039;&#039;error.message&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Auftrag lesen (liefert den ETag-Header):&lt;br /&gt;
&lt;br /&gt;
 curl -i -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Authorization: Bearer eyJ...&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/orders/4711&lt;br /&gt;
 ... ETag: 7&lt;br /&gt;
&lt;br /&gt;
ändern mit korrektem If-Match -&amp;gt; 200, mit veraltetem If-Match -&amp;gt; 409:&lt;br /&gt;
&lt;br /&gt;
 curl -i -X PUT -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Authorization: Bearer eyJ...&amp;quot; ^&lt;br /&gt;
      -H &amp;quot;If-Match: 7&amp;quot; -H &amp;quot;Content-Type: application/json&amp;quot; ^&lt;br /&gt;
      -d &amp;quot;{\&amp;quot;status\&amp;quot;:\&amp;quot;erledigt\&amp;quot;}&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/orders/4711&lt;br /&gt;
&lt;br /&gt;
Material idempotent erfassen (zweiter Aufruf mit gleichem Key -&amp;gt; gleiche Antwort, keine Doppelbuchung):&lt;br /&gt;
&lt;br /&gt;
 curl -i -X POST -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Authorization: Bearer eyJ...&amp;quot; ^&lt;br /&gt;
      -H &amp;quot;Idempotency-Key: 9c84-7f2a-...&amp;quot; -H &amp;quot;Content-Type: application/json&amp;quot; ^&lt;br /&gt;
      -d &amp;quot;{\&amp;quot;positionen\&amp;quot;:[{\&amp;quot;artikel\&amp;quot;:\&amp;quot;A100\&amp;quot;,\&amp;quot;menge\&amp;quot;:3},{\&amp;quot;artikel\&amp;quot;:\&amp;quot;B200\&amp;quot;,\&amp;quot;menge\&amp;quot;:1}]}&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/orders/4711/material&lt;br /&gt;
&lt;br /&gt;
Die Antwort des zweiten Aufrufs trägt zusätzlich den Header&lt;br /&gt;
&#039;&#039;Idempotent-Replay: true&#039;&#039; - daran ist erkennbar, dass sie aus dem Speicher kam&lt;br /&gt;
und nichts erneut gebucht wurde.&lt;br /&gt;
&lt;br /&gt;
Token erneuern (Refresh-Token im Authorization-Header an denselben JWT-Endpunkt):&lt;br /&gt;
&lt;br /&gt;
 curl -X POST -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Authorization: Bearer &amp;lt;refreshToken&amp;gt;&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/auth/&lt;br /&gt;
&lt;br /&gt;
Die Antwort enthält ein neues Paar. Das alte Refresh-Token ist damit verbraucht:&lt;br /&gt;
ein zweiter Aufruf mit demselben Token antwortet &#039;&#039;&#039;401&#039;&#039;&#039;. Erfolgt er innerhalb&lt;br /&gt;
von 60 Sekunden, bleibt die Sitzung bestehen (paralleler Refresh der App); später&lt;br /&gt;
gilt er als Wiedervorlage und die &#039;&#039;&#039;ganze Sitzung wird gesperrt&#039;&#039;&#039; - der&lt;br /&gt;
Techniker muss sich neu anmelden, und der Vorfall steht mit IP im Protokoll.&lt;br /&gt;
&lt;br /&gt;
Abmelden - DELETE auf denselben JWT-Endpunkt, Access-Token im Header:&lt;br /&gt;
&lt;br /&gt;
 curl -i -X DELETE -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Authorization: Bearer eyJ...&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/auth/&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 204 No Content&lt;br /&gt;
&lt;br /&gt;
Derselbe Token danach noch einmal verwendet:&lt;br /&gt;
&lt;br /&gt;
 curl -i -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Authorization: Bearer eyJ...&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/orders/4711&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 401 Unauthorized&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;error&amp;quot;: {&lt;br /&gt;
    &amp;quot;code&amp;quot;:    &amp;quot;AUTH_EXPIRED&amp;quot;,&lt;br /&gt;
    &amp;quot;uid&amp;quot;:     &amp;quot;Y00G9TOK09&amp;quot;,&lt;br /&gt;
    &amp;quot;message&amp;quot;: &amp;quot;Die Sitzung ist nicht mehr gültig, bitte neu anmelden&amp;quot;,&lt;br /&gt;
    &amp;quot;traceId&amp;quot;: &amp;quot;20260819T091233123-00001A&amp;quot;&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Was zeigt das Beispiel?==&lt;br /&gt;
&lt;br /&gt;
* Echte HTTP-Statuscodes aus dem Skript: 201, 403, 404, 409, 422, 500.&lt;br /&gt;
* Eine &#039;&#039;&#039;Liste aus dem Körper lesen&#039;&#039;&#039; über &#039;&#039;oReader.Count&#039;&#039; und Pfade wie &#039;&#039;positionen[0].artikel&#039;&#039; - ohne Cast, ohne &#039;&#039;.Items[i]&#039;&#039;.&lt;br /&gt;
* Eine &#039;&#039;&#039;benannte Liste von Objekten schreiben&#039;&#039;&#039; (&#039;&#039;ArrBegin&#039;&#039; / &#039;&#039;ObjBegin(&#039;&#039;)&#039;&#039; / &#039;&#039;ArrEnd&#039;&#039;) und &#039;&#039;&#039;Kommazahlen&#039;&#039;&#039; über &#039;&#039;oWriter.Num(name, wert, 2)&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;Erst prüfen, dann schreiben&#039;&#039;&#039;: Alle Positionen werden validiert, bevor die erste gebucht wird - sonst hinterlässt ein Fehler in Position 3 zwei gebuchte Zeilen und eine Fehlerantwort.&lt;br /&gt;
* &#039;&#039;&#039;Den Stapel schließen, bevor ein Fehler gesetzt wird&#039;&#039;&#039; - ein offener &#039;&#039;ArrBegin&#039;&#039; ist auch bei &#039;&#039;oWriter.Error&#039;&#039; ein Strukturfehler.&lt;br /&gt;
* Jeder Schreibzugriff wird auf Erfolg geprüft - ein gescheitertes &#039;&#039;SaveData&#039;&#039; darf nicht als 200 oder 201 beim Client ankommen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting#Schreibfehler erkennen|Schreibfehler erkennen]]).&lt;br /&gt;
* Optimistic Concurrency über &#039;&#039;ETag&#039;&#039; (GET) und &#039;&#039;If-Match&#039;&#039; (PUT) -&amp;gt; 409 VERSION_CONFLICT.&lt;br /&gt;
* Idempotente Schreibzugriffe über den &#039;&#039;Idempotency-Key&#039;&#039; - &#039;&#039;&#039;vom Server erledigt&#039;&#039;&#039;, das Skript enthält dafür keine Zeile Code.&lt;br /&gt;
* Rollenprüfung aus dem Token-Claim über &#039;&#039;oReader.Claim(&#039;roles&#039;)&#039;&#039;, nicht aus dem Body.&lt;br /&gt;
* JWT mit Custom-Claims (&#039;&#039;tenant&#039;&#039;/&#039;&#039;roles&#039;&#039;) und Refresh-Token mit Rotation - &#039;&#039;&#039;vom Server erledigt&#039;&#039;&#039;, das Skript führt keine Sperrtabelle.&lt;br /&gt;
* Anmelden, Erneuern und Abmelden über &#039;&#039;&#039;einen&#039;&#039;&#039; Endpunkt - Abmelden als &#039;&#039;DELETE&#039;&#039;, ohne eigene Endpunkt-Zeile und ohne Skript.&lt;br /&gt;
* &#039;&#039;oWriter.JwtRevoke&#039;&#039; für den Fall, dass beim Abmelden zusätzlich Fachlogik laufen soll oder alle Geräte gemeint sind.&lt;br /&gt;
* &#039;&#039;traceId&#039;&#039; im Fehler-Body für die Support-Nachverfolgung (auch als Header &#039;&#039;X-Trace-Id&#039;&#039;).&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel6&amp;diff=67345</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Beispiel6</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel6&amp;diff=67345"/>
		<updated>2026-11-05T12:39:57Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Navigationsvorlage mit Vorlage:-Praefix [Volltext ersetzt] [Styleguide-Abweichung: Seitenkopf der Altseite]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Beispiel 6: Datei-Upload und Ablage=&lt;br /&gt;
&lt;br /&gt;
Dieses Beispiel zeigt einen Endpunkt, der &#039;&#039;&#039;Dateien entgegennimmt&#039;&#039;&#039;: Ein&lt;br /&gt;
Aussendienst-Mitarbeiter fotografiert vor Ort ein Gerät, die App lädt das Foto zum&lt;br /&gt;
Auftrag hoch, der Server legt es ab und verknüpft es mit dem Vorgang.&lt;br /&gt;
&lt;br /&gt;
Der Ablauf ist derselbe für Belege aus einem Scanner, Unterschriften-Bilder oder&lt;br /&gt;
Prüfprotokolle als PDF. Behandelt werden beide Übertragungsarten - der einfache&lt;br /&gt;
Upload für kleine Dateien und der fortsetzbare Upload für grosse.&lt;br /&gt;
&lt;br /&gt;
==Einrichtung in OBS==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Server-Profil:&#039;&#039;&#039; Standard-TLS-Profil &#039;&#039;Public-API&#039;&#039; (Port 443)&lt;br /&gt;
* &#039;&#039;&#039;Zugang:&#039;&#039;&#039; &#039;&#039;Mobile-App&#039;&#039;, API-Key zufällig generiert, JWT aktiv&lt;br /&gt;
* &#039;&#039;&#039;Endpunkt:&#039;&#039;&#039; &#039;&#039;/orders/{uid}/photos&#039;&#039;, Profil &#039;&#039;Public-API&#039;&#039;&lt;br /&gt;
** &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; auf &#039;&#039;&#039;Ja&#039;&#039;&#039; setzen (&amp;lt;code&amp;gt;re_upload = 1&amp;lt;/code&amp;gt;)&lt;br /&gt;
** &#039;&#039;&#039;Max. Upload-Grösse&#039;&#039;&#039; auf &#039;&#039;20&#039;&#039; (MB) setzen&lt;br /&gt;
* &#039;&#039;&#039;Berechtigung:&#039;&#039;&#039; Zugang &#039;&#039;Mobile-App&#039;&#039; für den Endpunkt freischalten&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ohne den Haken &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; lehnt der Server jeden Upload mit&lt;br /&gt;
&#039;&#039;&#039;415&#039;&#039;&#039; ab und das Skript läuft gar nicht erst an. Das ist die häufigste Ursache,&lt;br /&gt;
wenn ein Upload &amp;quot;ohne erkennbaren Grund&amp;quot; scheitert.}}&lt;br /&gt;
&lt;br /&gt;
==Was der Server erledigt, bevor das Skript läuft==&lt;br /&gt;
&lt;br /&gt;
Der Server nimmt die Datei entgegen, prüft die Grösse, legt sie in einem temporären&lt;br /&gt;
Verzeichnis ab und übergibt dem Skript drei Parameter:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Inhalt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadPath()&amp;lt;/code&amp;gt; || Vollständiger Pfad zur temporären Datei&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadName()&amp;lt;/code&amp;gt; || Originaldateiname, wie ihn der Client gesendet hat&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadType()&amp;lt;/code&amp;gt; || Content-Type der Datei&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadSha()&amp;lt;/code&amp;gt; || SHA-256 der gespeicherten Datei (hex, klein)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Zwei Dinge sind beim Schreiben des Skripts wichtig:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Bei Uploads gibt es keinen JSON-Körper&#039;&#039;&#039; - &amp;lt;code&amp;gt;oReader.HasBody()&amp;lt;/code&amp;gt; ist &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, der Request-Body ist die Datei. Zusatzangaben (Kategorie, Bemerkung) kommen deshalb als &#039;&#039;&#039;Query-Parameter&#039;&#039;&#039; mit - oder, bei &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt;, als weitere &#039;&#039;&#039;Formularfelder&#039;&#039;&#039;, lesbar über &amp;lt;code&amp;gt;oReader.Form(&#039;&amp;amp;lt;name&amp;amp;gt;&#039;)&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Die temporäre Datei bleibt liegen, wenn das Skript sie nicht übernimmt.&#039;&#039;&#039; Der Server räumt sie nicht selbst weg.&lt;br /&gt;
&lt;br /&gt;
==Endpunkt-Skript &#039;&#039;/orders/{uid}/photos&#039;&#039;==&lt;br /&gt;
&lt;br /&gt;
Das Skript prüft den Dateityp, baut einen Zielpfad aus Auftrag und Zeitstempel,&lt;br /&gt;
verschiebt die Datei dorthin und schreibt einen Verweis in die Datenbank.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
const ZIEL_BASIS = &#039;D:\OBS\Daten\Auftragsfotos\&#039;;&lt;br /&gt;
var cUid      : string;&lt;br /&gt;
    cTmpPfad  : string;&lt;br /&gt;
    cOrigName : string;&lt;br /&gt;
    cTyp      : string;&lt;br /&gt;
    cEndung   : string;&lt;br /&gt;
    cKategorie: string;&lt;br /&gt;
    cZielDir  : string;&lt;br /&gt;
    cZielName : string;&lt;br /&gt;
    cZielPfad : string;&lt;br /&gt;
    qNeu      : TqSQL;&lt;br /&gt;
begin&lt;br /&gt;
    cUid       := oReader.Path(&#039;uid&#039;);&lt;br /&gt;
    cTmpPfad   := oReader.UploadPath();&lt;br /&gt;
    cOrigName  := oReader.UploadName();&lt;br /&gt;
    cTyp       := oReader.UploadType();&lt;br /&gt;
    cKategorie := oReader.Param(&#039;kategorie&#039;);   // Query-Parameter, nicht Body&lt;br /&gt;
&lt;br /&gt;
    // 1) Gehoert der Auftrag zum Mandanten aus dem Token?&lt;br /&gt;
    if (not AuftragGehoertZuMandant(oReader.Claim(&#039;tenant&#039;), cUid)) then begin&lt;br /&gt;
        oWriter.Error(404, &#039;NOT_FOUND&#039;, &#039;Auftrag nicht gefunden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // 2) Dateityp gegen eine Whitelist pruefen - niemals gegen eine&lt;br /&gt;
    //    Blacklist, und niemals dem Content-Type allein vertrauen.&lt;br /&gt;
    cEndung := Lower(ExtractFileExt(cOrigName));&lt;br /&gt;
    if ((cEndung &amp;lt;&amp;gt; &#039;.jpg&#039;) and (cEndung &amp;lt;&amp;gt; &#039;.jpeg&#039;) and (cEndung &amp;lt;&amp;gt; &#039;.png&#039;) and (cEndung &amp;lt;&amp;gt; &#039;.pdf&#039;)) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Nur JPG, PNG und PDF sind zulässig&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // 3) Zielname selbst bilden. Den Originalnamen NICHT als Pfad&lt;br /&gt;
    //    verwenden - er kommt vom Client und kann &#039;..\&#039; enthalten.&lt;br /&gt;
    cZielDir  := ZIEL_BASIS + DToS(Date()) + &#039;\&#039; + cUid + &#039;\&#039;;&lt;br /&gt;
    cZielName := DToSF(Now()) + &#039;_&#039; + GlobalUID() + cEndung;&lt;br /&gt;
    cZielPfad := cZielDir + cZielName;&lt;br /&gt;
&lt;br /&gt;
    MyForceDirectories(cZielDir);&lt;br /&gt;
&lt;br /&gt;
    if (not FMove(cTmpPfad, cZielPfad)) then begin&lt;br /&gt;
        // Fehlgeschlagenes Verschieben ist ein Serverproblem -&amp;gt; 500.&lt;br /&gt;
        // Der Idempotency-Key wird bei 5xx wieder freigegeben, der&lt;br /&gt;
        // Client darf denselben Schluessel erneut senden.&lt;br /&gt;
        oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Datei konnte nicht abgelegt werden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // 4) Verweis in der Datenbank ablegen&lt;br /&gt;
    qNeu := qSqlInit(oDB, &#039;AUFTRAG_DOKUMENT&#039;);&lt;br /&gt;
    qNeu.qSet(&#039;ad_auftrag&#039;  , cUid);&lt;br /&gt;
    qNeu.qSet(&#039;ad_pfad&#039;     , cZielPfad);&lt;br /&gt;
    qNeu.qSet(&#039;ad_dateiname&#039;, cOrigName);&lt;br /&gt;
    qNeu.qSet(&#039;ad_typ&#039;      , cTyp);&lt;br /&gt;
    qNeu.qSet(&#039;ad_kategorie&#039;, cKategorie);&lt;br /&gt;
    qNeu.qSet(&#039;ad_datum&#039;    , Now());&lt;br /&gt;
    // Rueckgabewert auswerten: ohne das bestaetigt der Endpunkt eine&lt;br /&gt;
    // Ablage, die es nicht gibt - die Datei liegt dann im Zielverzeichnis,&lt;br /&gt;
    // aber ohne Verweis in der Datenbank.&lt;br /&gt;
    if (not qNeu.SaveData(NEW_RECORD)) then begin&lt;br /&gt;
        qSqlFree(qNeu);&lt;br /&gt;
        oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Verweis konnte nicht gespeichert werden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
    qSqlFree(qNeu);&lt;br /&gt;
&lt;br /&gt;
    // 5) 201 mit Location auf die abgelegte Datei&lt;br /&gt;
    oWriter.Str(&#039;dateiname&#039;, cZielName);&lt;br /&gt;
    oWriter.Int(&#039;groesse&#039;  , FSize(cZielPfad));&lt;br /&gt;
    oWriter.Status(201);&lt;br /&gt;
    oWriter.Header(&#039;Location&#039;, &#039;/orders/&#039; + cUid + &#039;/photos/&#039; + cZielName);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Soll die Datei statt in ein Verzeichnis in das &#039;&#039;&#039;OBS-Dokumenten-System&#039;&#039;&#039;&lt;br /&gt;
wandern, ersetzt der DMS-Aufruf die Schritte 3 bis 5. Welche Funktion dafür&lt;br /&gt;
zuständig ist, hängt vom eingesetzten Dokumenttyp ab - bitte mit dem OBS-Support&lt;br /&gt;
klären.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Fünf Fehlerausgänge, fünf Zeilen.&#039;&#039;&#039; Vorher baute jeder von ihnen&lt;br /&gt;
sein &#039;&#039;error&#039;&#039;-Objekt, seinen Statuscode und seine &#039;&#039;traceId&#039;&#039; von Hand - fünfmal&lt;br /&gt;
dasselbe Muster, das an einer Stelle abweichen kann, ohne dass es auffällt.&lt;br /&gt;
&amp;lt;code&amp;gt;oWriter.Error&amp;lt;/code&amp;gt; setzt alles davon, auch die &#039;&#039;traceId&#039;&#039;, und verwirft&lt;br /&gt;
einen bereits begonnenen Körper. Ein &#039;&#039;exit&#039;&#039; danach genügt.}}&lt;br /&gt;
&lt;br /&gt;
==Test mit curl==&lt;br /&gt;
&lt;br /&gt;
Einfacher Upload einer Datei (&#039;&#039;-F&#039;&#039; erzeugt &#039;&#039;multipart/form-data&#039;&#039;):&lt;br /&gt;
&lt;br /&gt;
 curl -i -X POST -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Authorization: Bearer eyJ...&amp;quot; ^&lt;br /&gt;
      -F &amp;quot;datei=@C:\Fotos\geraet.jpg&amp;quot; ^&lt;br /&gt;
      &amp;quot;https://api.meinserver.de/orders/4711/photos?kategorie=VORHER&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Antwort:&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 201 Created&lt;br /&gt;
 Location: /orders/4711/photos/20260817091233_A1B2C3.jpg&lt;br /&gt;
 X-Trace-Id: 20260817T091233123-00001A&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;dateiname&amp;quot;:&amp;quot;20260817091233_A1B2C3.jpg&amp;quot;,&amp;quot;groesse&amp;quot;:248113}&lt;br /&gt;
&lt;br /&gt;
Ein Upload an einen Endpunkt &#039;&#039;&#039;ohne&#039;&#039;&#039; Upload-Freigabe:&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 415 Unsupported Media Type&lt;br /&gt;
&lt;br /&gt;
Eine Datei über der eingestellten Grenze:&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 413 Payload Too Large&lt;br /&gt;
&lt;br /&gt;
==Grosse Dateien: fortsetzbarer Upload==&lt;br /&gt;
&lt;br /&gt;
Bei einem Foto aus dem Mobilfunknetz reisst die Verbindung schnell einmal ab. Für&lt;br /&gt;
solche Fälle überträgt der Client die Datei in Teilstücken und kann nach einem&lt;br /&gt;
Abbruch dort weitermachen, wo er aufgehört hat. &#039;&#039;&#039;Am Endpunkt-Skript ändert sich&lt;br /&gt;
dafür nichts&#039;&#039;&#039; - der Server sammelt die Teilstücke selbst ein und ruft das Skript&lt;br /&gt;
erst auf, wenn die Datei vollständig ist.&lt;br /&gt;
&lt;br /&gt;
Der erste Teil trägt den Dateinamen (Base64-kodiert im Header&lt;br /&gt;
&amp;lt;code&amp;gt;Upload-Metadata&amp;lt;/code&amp;gt;) und liefert eine &amp;lt;code&amp;gt;Upload-Id&amp;lt;/code&amp;gt; zurück:&lt;br /&gt;
&lt;br /&gt;
 curl -i -X POST -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Authorization: Bearer eyJ...&amp;quot; ^&lt;br /&gt;
      -H &amp;quot;Content-Range: bytes 0-1048575/5000000&amp;quot; ^&lt;br /&gt;
      -H &amp;quot;Upload-Metadata: filename Z2VyYWV0LmpwZw==,filetype aW1hZ2UvanBlZw==&amp;quot; ^&lt;br /&gt;
      --data-binary &amp;quot;@teil1.bin&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/orders/4711/photos&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 308 Resume Incomplete&lt;br /&gt;
 Upload-Id: 7f2a9c84...&lt;br /&gt;
 Upload-Offset: 1048576&lt;br /&gt;
 Range: bytes=0-1048575&lt;br /&gt;
&lt;br /&gt;
Jeder weitere Teil sendet die &#039;&#039;Upload-Id&#039;&#039; mit. Der letzte Teil bekommt die&lt;br /&gt;
normale Antwort des Skripts (hier &#039;&#039;&#039;201&#039;&#039;&#039;). Nach einem Abbruch fragt der Client&lt;br /&gt;
den Stand ab und setzt dort auf:&lt;br /&gt;
&lt;br /&gt;
 curl -i -X POST -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Authorization: Bearer eyJ...&amp;quot; ^&lt;br /&gt;
      -H &amp;quot;Content-Range: bytes */5000000&amp;quot; -H &amp;quot;Upload-Id: 7f2a9c84...&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/orders/4711/photos&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 308 Resume Incomplete&lt;br /&gt;
 Upload-Offset: 3145728&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Zwischenstand liegt im Arbeitsspeicher des Dienstes. Wird der&lt;br /&gt;
REST-Dienst neu gestartet, muss ein unvollständiger Upload von vorn beginnen.}}&lt;br /&gt;
&lt;br /&gt;
==Doppelte Uploads vermeiden==&lt;br /&gt;
&lt;br /&gt;
Bricht die Verbindung ab, nachdem der Server die Datei schon abgelegt hat, würde ein&lt;br /&gt;
Wiederholversuch dasselbe Foto ein zweites Mal einliefern. Dagegen sendet der Client&lt;br /&gt;
den Header &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; - für jedes Foto einen eigenen, über alle&lt;br /&gt;
Wiederholversuche hinweg denselben:&lt;br /&gt;
&lt;br /&gt;
 -H &amp;quot;Idempotency-Key: foto-4711-0003&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Der Server erkennt die Wiederholung und liefert die &#039;&#039;&#039;gespeicherte 201-Antwort&#039;&#039;&#039;&lt;br /&gt;
samt Header &amp;lt;code&amp;gt;Idempotent-Replay: true&amp;lt;/code&amp;gt; zurück, ohne das Skript erneut zu&lt;br /&gt;
starten. Es entsteht also weder eine zweite Datei noch ein zweiter DB-Eintrag.&lt;br /&gt;
Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]], Abschnitt&lt;br /&gt;
Idempotenz.&lt;br /&gt;
&lt;br /&gt;
==Was zeigt das Beispiel?==&lt;br /&gt;
&lt;br /&gt;
* Endpunkt mit &#039;&#039;&#039;Datei-Upload-Freigabe&#039;&#039;&#039; (&amp;lt;code&amp;gt;re_upload&amp;lt;/code&amp;gt;) und eigener Grössenbegrenzung.&lt;br /&gt;
* Zugriff auf die hochgeladene Datei über &#039;&#039;oReader.UploadPath()&#039;&#039;, &#039;&#039;UploadName()&#039;&#039; und &#039;&#039;UploadType()&#039;&#039;.&lt;br /&gt;
* Zusatzangaben als &#039;&#039;&#039;Query-Parameter&#039;&#039;&#039; oder Formularfeld, weil bei Uploads kein JSON-Körper vorliegt.&lt;br /&gt;
* &#039;&#039;&#039;Whitelist&#039;&#039;&#039; für zulässige Dateitypen und ein &#039;&#039;&#039;selbst gebildeter Zielname&#039;&#039;&#039; - der Originalname landet nie im Pfad.&lt;br /&gt;
* Übernahme der temporären Datei mit &#039;&#039;FMove&#039;&#039;; ohne diesen Schritt bleibt sie liegen.&lt;br /&gt;
* Antwort mit &#039;&#039;&#039;201&#039;&#039;&#039; und &#039;&#039;Location&#039;&#039;-Header.&lt;br /&gt;
* Fortsetzbarer Upload ohne jede Änderung am Skript.&lt;br /&gt;
* Schutz gegen doppelte Einlieferung über den &#039;&#039;Idempotency-Key&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==Siehe auch==&lt;br /&gt;
&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]] - Upload-Parameter und Protokoll im Detail&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]] - Freigabe, Grössenlimit, Idempotenz&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel5|Beispiel 5]] - Schreibzugriff mit Statuscodes und ETag&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel3&amp;diff=67347</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Beispiel3</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel3&amp;diff=67347"/>
		<updated>2026-11-05T12:39:56Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Navigationsvorlage mit Vorlage:-Praefix [Volltext ersetzt] [Styleguide-Abweichung: Seitenkopf der Altseite]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Beispiel 3: Datensatz anlegen mit JSON-Body=&lt;br /&gt;
&lt;br /&gt;
Dieses Beispiel zeigt einen Endpunkt mit allen vier CRUD-Zugriffen (GET, POST, PUT und - anstelle von DELETE - PATCH, siehe den Hinweis beim Schliessen). Es geht um Tickets eines externen Servicedesks, der Tickets über den OBS REST-Server in OBS einliefern, abrufen, aktualisieren und schliessen kann.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Dieses Beispiel adressiert ein einzelnes Ticket über einen Query-Parameter (&#039;&#039;?id...&#039;&#039;). Alternativ lässt sich dieselbe Ressource über ein Pfad-Template wie &#039;&#039;/tickets/{id}&#039;&#039; ansprechen; die ID steht dann als &#039;&#039;oReader.Path(&#039;id&#039;)&#039;&#039; bereit. Ein vollständiges Beispiel dazu zeigt [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4 - Pfad-Parameter]].}}&lt;br /&gt;
&lt;br /&gt;
==Einrichtung in OBS==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Server-Profil:&#039;&#039;&#039; Standard-TLS-Profil &#039;&#039;Public-API&#039;&#039;, Bindung 0.0.0.0:443&lt;br /&gt;
* &#039;&#039;&#039;Zugang:&#039;&#039;&#039; &#039;&#039;Servicedesk-X&#039;&#039;&lt;br /&gt;
** API-Key: zufällig generiert&lt;br /&gt;
** Host: &#039;&#039;servicedesk.firma.de&#039;&#039; (DNS-gebunden, IP-Zugriff wird abgelehnt)&lt;br /&gt;
** JWT: nicht aktiv&lt;br /&gt;
* &#039;&#039;&#039;Endpunkt:&#039;&#039;&#039; &#039;&#039;tickets/v1&#039;&#039;, Profil &#039;&#039;Public-API&#039;&#039;&lt;br /&gt;
* &#039;&#039;&#039;Berechtigung:&#039;&#039;&#039; Zugang &#039;&#039;Servicedesk-X&#039;&#039; für Endpunkt &#039;&#039;tickets/v1&#039;&#039; freigeschaltet&lt;br /&gt;
&lt;br /&gt;
==Endpunkt-Skript &#039;&#039;tickets/v1&#039;&#039;==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
// Hilfsprozedur: die FELDER eines Tickets. Sie bekommt den Writer und schreibt&lt;br /&gt;
// in das Objekt, das der Aufrufer geoeffnet hat - einmal als Listeneintrag,&lt;br /&gt;
// einmal in die Wurzel. Ein Helfer, der ein Fragment als String zurueckgibt,&lt;br /&gt;
// waere der alte Weg; dabei ging die Reihenfolge im Zusammenbau verloren.&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure _TicketFelder(oWriter: TxRestWriter; qTicket: TxFQuery;&lt;br /&gt;
                        const cOffset: string);&lt;br /&gt;
begin&lt;br /&gt;
    oWriter.Str (&#039;id&#039;     , qTicket.A2UID());&lt;br /&gt;
    oWriter.Str (&#039;nr&#039;     , qTicket.A2C(&#039;ti_nr&#039;));&lt;br /&gt;
    oWriter.Str (&#039;betreff&#039;, qTicket.A2C(&#039;ti_betreff&#039;));&lt;br /&gt;
    oWriter.Str (&#039;status&#039; , qTicket.A2C(&#039;ti_status&#039;));&lt;br /&gt;
    oWriter.Str (&#039;beschr&#039; , qTicket.A2C(&#039;ti_beschr&#039;));&lt;br /&gt;
    // Zeitpunkte gehen als ISO-8601 MIT OFFSET ueber die Leitung, nicht im&lt;br /&gt;
    // SQL-Format: DTToSQL liefert &#039;JJJJ-MM-TT hh:mm:ss&#039; ohne Zeitzone, und der&lt;br /&gt;
    // Konsument muss dann raten, in welcher er steht. RestIsoFromDate nimmt den&lt;br /&gt;
    // Offset des Servers dazu. Ein leeres Datum ergibt null, nicht 1899.&lt;br /&gt;
    oWriter.StrN(&#039;aenderung&#039;, RestIsoFromDate(qTicket.A2D(&#039;ti_aend_dat&#039;), cOffset));&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
// Fuer den Body-Zugriff und fuer Fehlerantworten braucht es keine eigenen&lt;br /&gt;
// Hilfsfunktionen mehr: oReader liest typisiert, oWriter.Error baut die&lt;br /&gt;
// vollstaendige Fehlerhuelle samt traceId.&lt;br /&gt;
&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
// GET   /tickets/v1?id=...   - einzelnes Ticket&lt;br /&gt;
// GET   /tickets/v1          - alle offenen Tickets&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cSql     : string;&lt;br /&gt;
    qData    : TxFQuery;&lt;br /&gt;
    cId      : string;&lt;br /&gt;
    cOffset  : string;&lt;br /&gt;
    lGefunden: Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    cId     := oReader.Param(&#039;id&#039;);&lt;br /&gt;
    // Der Offset des Servers geht in jeden Zeitstempel der Antwort.&lt;br /&gt;
    cOffset := oReader.Offset();&lt;br /&gt;
&lt;br /&gt;
    if (not Empty(cId)) then begin&lt;br /&gt;
        lGefunden := false;&lt;br /&gt;
        cSql := &#039;SELECT * FROM tickets WHERE sys_uid = &#039; + DB_SQLVal(cId);&lt;br /&gt;
        if (DB_SOpen(oDB, cSql, qData)) then begin&lt;br /&gt;
            // EoF gehoert dazu: DB_SOpen sagt nur, dass die ABFRAGE lief.&lt;br /&gt;
            if (not qData.EoF) then begin&lt;br /&gt;
                _TicketFelder(oWriter, qData, cOffset);&lt;br /&gt;
                lGefunden := true;&lt;br /&gt;
            end;&lt;br /&gt;
        end;&lt;br /&gt;
        DB_Close(qData);&lt;br /&gt;
&lt;br /&gt;
        if (not lGefunden) then begin&lt;br /&gt;
            oWriter.Error(404, &#039;NOT_FOUND&#039;, &#039;Ticket nicht gefunden&#039;);&lt;br /&gt;
        end;&lt;br /&gt;
    end else begin&lt;br /&gt;
        // Die Liste ist eine nackte Liste. RootArr steht vor der Abfrage:&lt;br /&gt;
        // kein Treffer heisst leere Liste, nicht leeres Objekt.&lt;br /&gt;
        oWriter.RootArr();&lt;br /&gt;
        cSql := &#039;SELECT * FROM tickets WHERE ti_status &amp;lt;&amp;gt; &amp;quot;9&amp;quot; ORDER BY ti_aend_dat DESC&#039;;&lt;br /&gt;
        if (DB_SOpen(oDB, cSql, qData)) then begin&lt;br /&gt;
            while (not qData.EoF) do begin&lt;br /&gt;
                oWriter.ObjBegin(&#039;&#039;);&lt;br /&gt;
                    _TicketFelder(oWriter, qData, cOffset);&lt;br /&gt;
                oWriter.ObjEnd;&lt;br /&gt;
                qData.Next();&lt;br /&gt;
            end;&lt;br /&gt;
        end;&lt;br /&gt;
        DB_Close(qData);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
// POST  /tickets/v1          - neues Ticket anlegen&lt;br /&gt;
// Body: { &amp;quot;betreff&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;beschr&amp;quot;:&amp;quot;...&amp;quot; }&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var xTicket : TqSQL;&lt;br /&gt;
    cId     : string;&lt;br /&gt;
    cBetreff: string;&lt;br /&gt;
    cBeschr : string;&lt;br /&gt;
    lOk     : Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    // HasBody unterscheidet &amp;quot;kein Koerper&amp;quot; von &amp;quot;Koerper ohne das Feld&amp;quot; - das&lt;br /&gt;
    // erste ist ein 400, das zweite ein 422.&lt;br /&gt;
    if (not oReader.HasBody()) then begin&lt;br /&gt;
        oWriter.Error(400, &#039;BAD_REQUEST&#039;, &#039;JSON-Body erforderlich&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    cBetreff := &#039;&#039;;&lt;br /&gt;
    cBeschr  := &#039;&#039;;&lt;br /&gt;
    oReader.Str(&#039;betreff&#039;, cBetreff);&lt;br /&gt;
    oReader.Str(&#039;beschr&#039; , cBeschr);&lt;br /&gt;
&lt;br /&gt;
    if (Empty(cBetreff)) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Feld &amp;quot;betreff&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    cId := GetNewId(oDB);&lt;br /&gt;
&lt;br /&gt;
    xTicket := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
    xTicket.lNoSysUID := True;&lt;br /&gt;
    try&lt;br /&gt;
        xTicket.qSet(&#039;sys_uid&#039;    , cId);&lt;br /&gt;
        xTicket.qSet(&#039;ti_nr&#039;      , DB_NeuNum(oDB, &#039;tickets&#039;, &#039;ti_nr&#039;, NEUNUM_HOLE, &#039;&#039;, &#039;&#039;, 1, 999999, &#039;0&#039;));&lt;br /&gt;
        xTicket.qSet(&#039;ti_betreff&#039; , cBetreff);&lt;br /&gt;
        xTicket.qSet(&#039;ti_beschr&#039;  , cBeschr);&lt;br /&gt;
        xTicket.qSet(&#039;ti_status&#039;  , &#039;1&#039;);&lt;br /&gt;
        xTicket.qSet(&#039;ti_anl_dat&#039; , Now());&lt;br /&gt;
        xTicket.qSet(&#039;ti_aend_dat&#039;, Now());&lt;br /&gt;
        // SaveData liefert einen Boolean. Ein SQL-Fehler - etwa Duplicate entry&lt;br /&gt;
        // auf einem eindeutigen Index - wirft KEINE Exception und steht in&lt;br /&gt;
        // keinem Protokoll. Ohne diese Auswertung antwortet der Endpunkt mit&lt;br /&gt;
        // 201, ohne etwas angelegt zu haben.&lt;br /&gt;
        lOk := xTicket.SaveData(NEW_RECORD);&lt;br /&gt;
    finally&lt;br /&gt;
        qSqlFree(xTicket);&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    if (not lOk) then begin&lt;br /&gt;
        oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Ticket konnte nicht angelegt werden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    oWriter.Str(&#039;id&#039;    , cId);&lt;br /&gt;
    oWriter.Str(&#039;status&#039;, &#039;created&#039;);&lt;br /&gt;
    oWriter.Status(201);&lt;br /&gt;
    oWriter.Header(&#039;Location&#039;, &#039;/tickets/v1?id=&#039; + cId);&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
// PUT   /tickets/v1          - Ticket aktualisieren&lt;br /&gt;
// Body: { &amp;quot;id&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;status&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;beschr&amp;quot;:&amp;quot;...&amp;quot; }&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure Put(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var xTicket: TqSQL;&lt;br /&gt;
    lOk    : Boolean;&lt;br /&gt;
    cId    : string;&lt;br /&gt;
    cStat  : string;&lt;br /&gt;
    cBeschr: string;&lt;br /&gt;
begin&lt;br /&gt;
    if (not oReader.HasBody()) then begin&lt;br /&gt;
        oWriter.Error(400, &#039;BAD_REQUEST&#039;, &#039;JSON-Body erforderlich&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    cId     := &#039;&#039;;&lt;br /&gt;
    cStat   := &#039;&#039;;&lt;br /&gt;
    cBeschr := &#039;&#039;;&lt;br /&gt;
    oReader.Str(&#039;id&#039;    , cId);&lt;br /&gt;
    oReader.Str(&#039;status&#039;, cStat);&lt;br /&gt;
    oReader.Str(&#039;beschr&#039;, cBeschr);&lt;br /&gt;
&lt;br /&gt;
    if (Empty(cId)) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Feld &amp;quot;id&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    if (not DB_LSeek(oDB, &#039;tickets&#039;, &#039;sys_uid = &#039; + DB_SQLVal(cId))) then begin&lt;br /&gt;
        oWriter.Error(404, &#039;NOT_FOUND&#039;, &#039;Ticket nicht gefunden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // Zum Aendern qSqlInit mit sys_uid, nicht qSqlRead: qSqlRead liest den&lt;br /&gt;
    // Altsatz und schreibt nur Aenderungen - ein unveraenderter Wert erzeugt&lt;br /&gt;
    // gar kein Write und SaveData liefert False.&lt;br /&gt;
    xTicket := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
    xTicket.qSet(&#039;sys_uid&#039;, cId);&lt;br /&gt;
    try&lt;br /&gt;
        if (not Empty(cStat))   then xTicket.qSet(&#039;ti_status&#039;  , cStat);&lt;br /&gt;
        if (not Empty(cBeschr)) then xTicket.qSet(&#039;ti_beschr&#039;  , cBeschr);&lt;br /&gt;
        xTicket.qSet(&#039;ti_aend_dat&#039;, Now());&lt;br /&gt;
        lOk := xTicket.SaveData(UPDATE_RECORD);&lt;br /&gt;
    finally&lt;br /&gt;
        qSqlFree(xTicket);&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    if (not lOk) then begin&lt;br /&gt;
        oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Ticket konnte nicht geaendert werden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    oWriter.Str(&#039;status&#039;, &#039;updated&#039;);&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
// PATCH /tickets/v1?id=...   - Ticket schliessen (Soft-Delete via Status=9)&lt;br /&gt;
//&lt;br /&gt;
// NICHT als DELETE: &#039;Delete&#039; ist in der Skriptsprache eine Standardprozedur,&lt;br /&gt;
// eine eigene Prozedur dieses Namens laesst sich nicht uebersetzen. Solange das&lt;br /&gt;
// so ist, laeuft ein Loeschen ueber PATCH oder POST. Siehe den Hinweis unten.&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure Patch(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var xTicket: TqSQL;&lt;br /&gt;
    cId    : string;&lt;br /&gt;
    lOk    : Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    cId := oReader.Param(&#039;id&#039;);&lt;br /&gt;
    if (Empty(cId)) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Parameter &amp;quot;id&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    if (not DB_LSeek(oDB, &#039;tickets&#039;, &#039;sys_uid = &#039; + DB_SQLVal(cId))) then begin&lt;br /&gt;
        oWriter.Error(404, &#039;NOT_FOUND&#039;, &#039;Ticket nicht gefunden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    xTicket := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
    xTicket.qSet(&#039;sys_uid&#039;, cId);&lt;br /&gt;
    try&lt;br /&gt;
        xTicket.qSet(&#039;ti_status&#039;  , &#039;9&#039;);&lt;br /&gt;
        xTicket.qSet(&#039;ti_aend_dat&#039;, Now());&lt;br /&gt;
        lOk := xTicket.SaveData(UPDATE_RECORD);&lt;br /&gt;
    finally&lt;br /&gt;
        qSqlFree(xTicket);&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    if (not lOk) then begin&lt;br /&gt;
        oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Ticket konnte nicht geschlossen werden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    oWriter.Str(&#039;status&#039;, &#039;closed&#039;);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Ein DELETE-Endpunkt kann derzeit kein Skript haben.&#039;&#039;&#039; Der Server&lt;br /&gt;
ruft die Prozedur, die genauso heisst wie das HTTP-Verb - für DELETE also&lt;br /&gt;
&amp;lt;code&amp;gt;Delete&amp;lt;/code&amp;gt;. Dieser Name ist in der Skriptsprache belegt&lt;br /&gt;
(Standardprozedur zum Löschen aus einer Zeichenkette), und die Deklaration&lt;br /&gt;
lässt sich nicht übersetzen; die Meldung nennt dabei nur die Zeile, nicht die&lt;br /&gt;
Ursache. Bis das gelöst ist: das Löschen als &amp;lt;code&amp;gt;PATCH&amp;lt;/code&amp;gt; oder&lt;br /&gt;
&amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; mit einer Aktion abbilden. Das Abmelden am JWT-Endpunkt ist&lt;br /&gt;
davon nicht betroffen - es läuft als DELETE &#039;&#039;&#039;ohne&#039;&#039;&#039; Skript vollständig in&lt;br /&gt;
der Engine.}}&lt;br /&gt;
&lt;br /&gt;
==Aufruf mit curl==&lt;br /&gt;
&lt;br /&gt;
Liste aller offenen Tickets:&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; https://api.meinserver.de/tickets/v1&lt;br /&gt;
&lt;br /&gt;
Einzelnes Ticket:&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; &amp;quot;https://api.meinserver.de/tickets/v1?id=abc-123&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Neues Ticket anlegen:&lt;br /&gt;
&lt;br /&gt;
 curl -X POST -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Content-Type: application/json&amp;quot; ^&lt;br /&gt;
      -d &amp;quot;{\&amp;quot;betreff\&amp;quot;:\&amp;quot;Drucker offline\&amp;quot;,\&amp;quot;beschr\&amp;quot;:\&amp;quot;Etage 2, Raum 23\&amp;quot;}&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/tickets/v1&lt;br /&gt;
&lt;br /&gt;
Ticket aktualisieren:&lt;br /&gt;
&lt;br /&gt;
 curl -X PUT -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Content-Type: application/json&amp;quot; ^&lt;br /&gt;
      -d &amp;quot;{\&amp;quot;id\&amp;quot;:\&amp;quot;abc-123\&amp;quot;,\&amp;quot;status\&amp;quot;:\&amp;quot;2\&amp;quot;}&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/tickets/v1&lt;br /&gt;
&lt;br /&gt;
Ticket schliessen:&lt;br /&gt;
&lt;br /&gt;
 curl -X PATCH -H &amp;quot;apikey: [API-KEY]&amp;quot; &amp;quot;https://api.meinserver.de/tickets/v1?id=abc-123&amp;quot;&lt;br /&gt;
&lt;br /&gt;
==Aufruf aus PHP==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
$apikey = &#039;[API-KEY]&#039;;&lt;br /&gt;
$base   = &#039;https://api.meinserver.de/tickets/v1&#039;;&lt;br /&gt;
&lt;br /&gt;
function rest($method, $url, $apikey, $body = null) {&lt;br /&gt;
    $ch = curl_init();&lt;br /&gt;
    curl_setopt($ch, CURLOPT_URL           , $url);&lt;br /&gt;
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);&lt;br /&gt;
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST , $method);&lt;br /&gt;
    $headers = [&#039;apikey: &#039; . $apikey];&lt;br /&gt;
    if ($body !== null) {&lt;br /&gt;
        $headers[] = &#039;Content-Type: application/json&#039;;&lt;br /&gt;
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));&lt;br /&gt;
    }&lt;br /&gt;
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);&lt;br /&gt;
    $res = curl_exec($ch);&lt;br /&gt;
    curl_close($ch);&lt;br /&gt;
    return json_decode($res, true);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Neues Ticket anlegen&lt;br /&gt;
$neu = rest(&#039;POST&#039;, $base, $apikey, [&lt;br /&gt;
    &#039;betreff&#039; =&amp;gt; &#039;Drucker offline&#039;,&lt;br /&gt;
    &#039;beschr&#039;  =&amp;gt; &#039;Etage 2, Raum 23&#039;&lt;br /&gt;
]);&lt;br /&gt;
$id = $neu[&#039;id&#039;];&lt;br /&gt;
&lt;br /&gt;
// Status aktualisieren&lt;br /&gt;
rest(&#039;PUT&#039;, $base, $apikey, [&#039;id&#039; =&amp;gt; $id, &#039;status&#039; =&amp;gt; &#039;2&#039;]);&lt;br /&gt;
&lt;br /&gt;
// Liste auslesen&lt;br /&gt;
$alle = rest(&#039;GET&#039;, $base, $apikey);&lt;br /&gt;
foreach ($alle as $t) {&lt;br /&gt;
    echo $t[&#039;nr&#039;] . &#039; - &#039; . $t[&#039;betreff&#039;] . PHP_EOL;&lt;br /&gt;
}&lt;br /&gt;
?&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Was zeigt das Beispiel?==&lt;br /&gt;
&lt;br /&gt;
* Saubere Trennung der CRUD-Methoden in einem Endpunkt.&lt;br /&gt;
* Verwendung des JSON-Body für komplexere Eingangsdaten (POST, PUT).&lt;br /&gt;
* Verwendung von Query-Parametern für einfache Werte (GET, PATCH).&lt;br /&gt;
* Soft-Delete über Status-Änderung statt physischer Löschung - und warum er als PATCH und nicht als DELETE läuft.&lt;br /&gt;
* Eindeutige Audit-UIDs (&#039;&#039;Y00CXXXXxx&#039;&#039;) für jeden DB-Zugriff zur Nachvollziehbarkeit.&lt;br /&gt;
* Host-gebundener Zugang als zweite Sicherheitsebene neben dem API-Key.&lt;br /&gt;
* Alternativ: ID per Pfad-Parameter statt Query-Parameter (siehe [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4]]).&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel2&amp;diff=67348</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Beispiel2</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel2&amp;diff=67348"/>
		<updated>2026-11-05T12:39:55Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Navigationsvorlage mit Vorlage:-Praefix [Volltext ersetzt] [Styleguide-Abweichung: Seitenkopf der Altseite]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Beispiel 2: Zeiterfassung als komplette Webanwendung=&lt;br /&gt;
&lt;br /&gt;
Dieses Script dient zur Zeiterfassung über den REST-Server. Bei Seitenaufruf wird eine Liste von Kunden geladen und in einer Liste angezeigt. &lt;br /&gt;
&lt;br /&gt;
Durch Rechtsklick auf einen Eintrag wird eine Liste an zugehörigen Tätigkeiten geladen, die nach Klick einen Timer starten.&lt;br /&gt;
&lt;br /&gt;
Per Javascript werden Startzeit, Startdatum und andere Informationen wie Kunde und Personalnummer über die PHP-Scripte und das Endpunkt-Script in einer Datenbank gespeichert.&lt;br /&gt;
&lt;br /&gt;
Der Status wird hierbei auf &amp;quot;1&amp;quot; gesetzt. Pausenzeiten werden nach Beenden einer Pause in der Datenbank gespeichert.&lt;br /&gt;
&lt;br /&gt;
Durch Drücken des Stop-Buttons wird eine Lightbox mit einer Zusammenfassung aller zum Timer gehörenden Daten angezeigt. Hier hat man die Möglichkeit Tätigkeiten, abgelaufene Zeit und &lt;br /&gt;
&lt;br /&gt;
Pausenzeit nachträglich zu bearbeiten. Durch Klick auf &amp;quot;Speichern&amp;quot; oder Druck auf F2 werden Endzeit und Enddatum gespeichert und der Status auf &amp;quot;21&amp;quot; gesetzt.&lt;br /&gt;
&lt;br /&gt;
Nach Reload der Webseite wird anhand des Status in der Datenbank nach laufenden Timern gesucht und diese gegebenenfalls gestartet.&lt;br /&gt;
&lt;br /&gt;
= Endpunkt-Script =&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot;&amp;gt;&lt;br /&gt;
procedure _AddJob(oWriter: TxRestWriter; id: String);&lt;br /&gt;
var cSql        : String;&lt;br /&gt;
    A_Query     : TxFQuery;&lt;br /&gt;
begin&lt;br /&gt;
    cSql    :=  &#039;SELECT CONCAT(za_text, &amp;quot;:&amp;quot;,sys_uid) AS Job FROM zeiterfart &#039; +&lt;br /&gt;
                &#039;WHERE za_psnr = &#039; + DB_SQLVal(id) + &#039; AND za_inaktiv &amp;lt;&amp;gt; &amp;quot;1&amp;quot; &#039; +&lt;br /&gt;
                &#039;ORDER BY za_order&#039;;&lt;br /&gt;
    if (DB_xSOpen(&#039;Y009LHNBTY&#039;, oDB, cSql, A_Query, False)) then begin&lt;br /&gt;
        while (not A_Query.EoF) do begin&lt;br /&gt;
            oWriter.Str(&#039;&#039;, A_Query.A2C(&#039;Job&#039;));&lt;br /&gt;
            A_Query.Next();&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
    DB_Close(A_Query);&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
procedure _AddCustomer(oWriter: TxRestWriter);&lt;br /&gt;
var cSql        : String;&lt;br /&gt;
    A_Query     : TxFQuery;&lt;br /&gt;
begin&lt;br /&gt;
    cSql    :=&#039;  SELECT * FROM&#039; + &lt;br /&gt;
              &#039;  (SELECT&#039; +&lt;br /&gt;
              &#039;  ps_nr,&#039; +&lt;br /&gt;
              &#039;  ps_such AS Kunde,&#039; +&lt;br /&gt;
              &#039;  perssta.sys_uid AS ps_sys_uid,&#039; +&lt;br /&gt;
              &#039;  &amp;quot;PE&amp;quot; AS TYP&#039; +&lt;br /&gt;
              &#039;  FROM eigenschaften&#039; +&lt;br /&gt;
              &#039;  LEFT JOIN perssta ON se_ref1 AND perssta.sys_uid = se_ref2&#039; +&lt;br /&gt;
              &#039;  WHERE se_nr = 0087&#039; + &lt;br /&gt;
              &#039;  AND ps_nr &amp;lt;&amp;gt; 100000&#039; +&lt;br /&gt;
              &#039;  UNION SELECT p_nr AS ps_nr,&#039; +&lt;br /&gt;
              &#039;  p_name1 AS ps_such,&#039; +&lt;br /&gt;
              &#039;  projekte.sys_uid AS ps_sys_uid,&#039; +&lt;br /&gt;
              &#039;  &amp;quot;PR&amp;quot; AS TYP&#039; +&lt;br /&gt;
              &#039;  FROM eigenschaften&#039; +&lt;br /&gt;
              &#039;  LEFT JOIN projekte ON se_ref1 = p_nr&#039; +&lt;br /&gt;
              &#039;  AND projekte.sys_uid = se_ref2 WHERE se_nr = 0087)&#039; +&lt;br /&gt;
              &#039;  A WHERE ps_nr IS NOT NULL&#039; +&lt;br /&gt;
              &#039;  ORDER BY Kunde &#039;;&lt;br /&gt;
              &lt;br /&gt;
    if (DB_xSOpen(&#039;Y009LHNBTY&#039;, oDB, cSql, A_Query, False)) then begin&lt;br /&gt;
        while (not A_Query.EoF) do begin&lt;br /&gt;
            oWriter.Str(&#039;&#039;, A_Query.A2C(&#039;Kunde&#039;));&lt;br /&gt;
            A_Query.Next();&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
    DB_Close(A_Query);&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
procedure _AddID(oWriter: TxRestWriter; kunde: String);&lt;br /&gt;
var cSql        : String;&lt;br /&gt;
    A_Query     : TxFQuery;&lt;br /&gt;
begin&lt;br /&gt;
    cSql    :=&#039;  SELECT * FROM&#039; + &lt;br /&gt;
              &#039;  (SELECT&#039; +&lt;br /&gt;
              &#039;  ps_nr,&#039; +&lt;br /&gt;
              &#039;  ps_such AS Kunde,&#039; +&lt;br /&gt;
              &#039;  perssta.sys_uid AS ps_sys_uid,&#039; +&lt;br /&gt;
              &#039;  &amp;quot;PE&amp;quot; AS TYP&#039; +&lt;br /&gt;
              &#039;  FROM eigenschaften&#039; +&lt;br /&gt;
              &#039;  LEFT JOIN perssta ON se_ref1 AND perssta.sys_uid = se_ref2&#039; +&lt;br /&gt;
              &#039;  WHERE se_nr = 0087&#039; + &lt;br /&gt;
              &#039;  AND ps_nr &amp;lt;&amp;gt; 100000&#039; +&lt;br /&gt;
              &#039;  UNION SELECT p_nr AS ps_nr,&#039; +&lt;br /&gt;
              &#039;  p_name1 AS ps_such,&#039; +&lt;br /&gt;
              &#039;  projekte.sys_uid AS ps_sys_uid,&#039; +&lt;br /&gt;
              &#039;  &amp;quot;PR&amp;quot; AS TYP&#039; +&lt;br /&gt;
              &#039;  FROM eigenschaften&#039; +&lt;br /&gt;
              &#039;  LEFT JOIN projekte ON se_ref1 = p_nr&#039; +&lt;br /&gt;
              &#039;  AND projekte.sys_uid = se_ref2 WHERE se_nr = 0087)&#039; +&lt;br /&gt;
              &#039;  A WHERE ps_nr IS NOT NULL&#039; +&lt;br /&gt;
              &#039;  AND Kunde = &#039; + DB_SQLVal(kunde) +&lt;br /&gt;
              &#039;  ORDER BY Kunde &#039;;&lt;br /&gt;
              &lt;br /&gt;
    if (DB_xSOpen(&#039;Y009LHNBTY&#039;, oDB, cSql, A_Query, False)) then begin&lt;br /&gt;
        while (not A_Query.EoF) do begin&lt;br /&gt;
            oWriter.Str(&#039;&#039;, A_Query.A2C(&#039;ps_nr&#039;));&lt;br /&gt;
            A_Query.Next();&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
    DB_Close(A_Query);&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
procedure _AddCHECK(oWriter: TxRestWriter);&lt;br /&gt;
var cSql        : String;&lt;br /&gt;
    A_Query     : TxFQuery;&lt;br /&gt;
begin&lt;br /&gt;
    cSql    :=  &#039;SELECT ze_starttime, zeiterfuser.sys_uid AS sysuid, zk_psnr, &#039; +&lt;br /&gt;
                &#039;za_text, ps_such, ze_startdate FROM &#039; +&lt;br /&gt;
                &#039;zeiterfuser LEFT JOIN zeiterfkomm ON &#039; +&lt;br /&gt;
                &#039;zk_messunguid = zeiterfuser.SYS_UID LEFT JOIN zeiterfart &#039; +&lt;br /&gt;
                &#039;ON zeiterfart.sys_uid = zeiterfuser.ze_uid AND &#039; +&lt;br /&gt;
                &#039;za_psnr = zeiterfkomm.zk_psnr LEFT JOIN perssta ON &#039; +&lt;br /&gt;
                &#039;ps_nr = zeiterfkomm.zk_psnr WHERE ze_status = 01 &#039;;&lt;br /&gt;
              &lt;br /&gt;
    if (DB_xSOpen(&#039;Y009LHNBTY&#039;, oDB, cSql, A_Query, False)) then begin&lt;br /&gt;
        while (not A_Query.EoF) do begin&lt;br /&gt;
            oWriter.Str(&#039;&#039;, A_Query.A2C(&#039;ze_starttime&#039;));&lt;br /&gt;
            oWriter.Str(&#039;&#039;, A_Query.A2C(&#039;zk_psnr&#039;));&lt;br /&gt;
            oWriter.Str(&#039;&#039;, A_Query.A2C(&#039;za_text&#039;));&lt;br /&gt;
            oWriter.Str(&#039;&#039;, A_Query.A2C(&#039;ps_such&#039;));&lt;br /&gt;
            oWriter.Str(&#039;&#039;, A_Query.A2C(&#039;sysuid&#039;));&lt;br /&gt;
            oWriter.Str(&#039;&#039;, A_Query.A2C(&#039;ze_startdate&#039;));&lt;br /&gt;
            A_Query.Next();&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
    DB_Close(A_Query);&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
procedure _getPauseTime(oWriter: TxRestWriter; sysuid: String);&lt;br /&gt;
var cSql        : String;&lt;br /&gt;
    A_Query     : TxFQuery;&lt;br /&gt;
begin&lt;br /&gt;
    cSql    :=  &#039;SELECT * FROM zeiterfuser WHERE sys_uid = &#039; + DB_SQLVal(sysuid);&lt;br /&gt;
    &lt;br /&gt;
    if (DB_xSOpen(&#039;Y009LHNBTY&#039;, oDB, cSql, A_Query, False)) then begin&lt;br /&gt;
        while (not A_Query.EoF) do begin&lt;br /&gt;
            oWriter.Str(&#039;&#039;, A_Query.A2C(&#039;ze_pausezeit&#039;));&lt;br /&gt;
            A_Query.Next();&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
    DB_Close(A_Query);&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
//----------------Funktionen----------------------------------//&lt;br /&gt;
&lt;br /&gt;
procedure POST(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cChoice: String;&lt;br /&gt;
begin&lt;br /&gt;
    cChoice := oReader.Param(&#039;choice&#039;);&lt;br /&gt;
&lt;br /&gt;
    // Jede dieser Antworten ist eine nackte Liste von Zeichenketten - genau&lt;br /&gt;
    // so, wie das Javascript sie mit JSON.parse erwartet. RootArr sagt das&lt;br /&gt;
    // und steht deshalb VOR dem ersten Eintrag.&lt;br /&gt;
    if (cChoice = &#039;job&#039;) then begin&lt;br /&gt;
        oWriter.RootArr();&lt;br /&gt;
        _AddJob(oWriter, oReader.Param(&#039;id&#039;));&lt;br /&gt;
    end else if (cChoice = &#039;customer&#039;) then begin&lt;br /&gt;
        oWriter.RootArr();&lt;br /&gt;
        _AddCustomer(oWriter);&lt;br /&gt;
    end else if (cChoice = &#039;id&#039;) then begin&lt;br /&gt;
        oWriter.RootArr();&lt;br /&gt;
        _AddID(oWriter, oReader.Param(&#039;selectedKunde&#039;));&lt;br /&gt;
    end else if (cChoice = &#039;check&#039;) then begin&lt;br /&gt;
        oWriter.RootArr();&lt;br /&gt;
        _AddCHECK(oWriter);&lt;br /&gt;
    end else if (cChoice = &#039;pauseTime&#039;) then begin&lt;br /&gt;
        oWriter.RootArr();&lt;br /&gt;
        _GetPauseTime(oWriter, oReader.Param(&#039;sysuid&#039;));&lt;br /&gt;
    end else begin&lt;br /&gt;
        // Eine unbekannte Auswahl war vorher eine leere Antwort - der Aufrufer&lt;br /&gt;
        // sah einen Tippfehler nicht. Jetzt sagt es der Statuscode.&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Unbekannte Auswahl: &#039; + cChoice);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
procedure PUT(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var xZeit        : TqSQL;&lt;br /&gt;
    cUSER_SYSUID : String;&lt;br /&gt;
    cKOMM_SYSUID : String;&lt;br /&gt;
    lNew         : Boolean;&lt;br /&gt;
    cSql         : String;&lt;br /&gt;
    dDBNow       : TDateTime;&lt;br /&gt;
    cChoice      : String;&lt;br /&gt;
begin&lt;br /&gt;
    cChoice := oReader.Param(&#039;choice&#039;);&lt;br /&gt;
    writeln(&#039;Choice: &#039; + cChoice);&lt;br /&gt;
&lt;br /&gt;
    if (cChoice = &#039;start&#039;) then begin&lt;br /&gt;
        dDBNow  := DB_Now(oDB);&lt;br /&gt;
        lNew    := True;&lt;br /&gt;
        if (empty(cUSER_SYSUID)) then begin&lt;br /&gt;
            cUSER_SYSUID := GetNewId(oDB);&lt;br /&gt;
        end;&lt;br /&gt;
&lt;br /&gt;
        xZeit           := qSqlInit(oDB, &#039;zeiterfuser&#039;);&lt;br /&gt;
        xZeit.lNoSysUID := True;&lt;br /&gt;
&lt;br /&gt;
        writeln(&#039;SYSUID: &#039; + oReader.Param(&#039;jobID&#039;));&lt;br /&gt;
&lt;br /&gt;
        xZeit.qSet(&#039;ze_user&#039;            , oReader.Param(&#039;user&#039;));&lt;br /&gt;
        xZeit.qSet(&#039;ze_starttime&#039;       , oReader.Param(&#039;uhrzeit&#039;));&lt;br /&gt;
        xZeit.qSet(&#039;ze_eigen&#039;           , &#039;0101&#039;);&lt;br /&gt;
        xZeit.qSet(&#039;ze_startdate&#039;       , oReader.Param(&#039;datum&#039;));&lt;br /&gt;
        xZeit.qSet(&#039;ze_endtime&#039;         , oReader.Param(&#039;endTime&#039;));&lt;br /&gt;
        xZeit.qSet(&#039;ze_enddate&#039;         , oReader.Param(&#039;endDate&#039;));&lt;br /&gt;
        xZeit.qSet(&#039;ze_zeit&#039;            , &#039;0&#039;);&lt;br /&gt;
        xZeit.qSet(&#039;ze_pausezeit&#039;       , &#039;0&#039;);&lt;br /&gt;
        xZeit.qSet(&#039;ze_status&#039;          , oReader.Param(&#039;status&#039;));&lt;br /&gt;
        xZeit.qSet(&#039;ze_berechnen&#039;       , 0);&lt;br /&gt;
        xZeit.qSet(&#039;ze_officestar&#039;      , 0);&lt;br /&gt;
        xZeit.qSet(&#039;ze_repa&#039;            , 0);&lt;br /&gt;
        xZeit.qSet(&#039;ze_vorbereitung&#039;    , 0);&lt;br /&gt;
        xZeit.qSet(&#039;ze_uid&#039;             , oReader.Param(&#039;jobID&#039;));&lt;br /&gt;
        xZeit.qSet(&#039;sys_uid&#039;            , cUSER_SYSUID);&lt;br /&gt;
&lt;br /&gt;
        // SaveData meldet einen Fehler ueber den RUECKGABEWERT, nicht ueber eine&lt;br /&gt;
        // Exception. Ohne diese Pruefung antwortet der Endpunkt mit Erfolg,&lt;br /&gt;
        // obwohl nichts geschrieben wurde.&lt;br /&gt;
        if (not xZeit.SaveData(lNew)) then begin&lt;br /&gt;
            qSQLFree(xZeit);&lt;br /&gt;
            oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Zeiterfassung konnte nicht gespeichert werden&#039;);&lt;br /&gt;
            exit;&lt;br /&gt;
        end;&lt;br /&gt;
        qSQLFree(xZeit);&lt;br /&gt;
&lt;br /&gt;
        //---------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
        xZeit := qSqlInit(oDB,&#039;ZEITERFKOMM&#039;);&lt;br /&gt;
        xZeit.lNoSysUID := True;&lt;br /&gt;
&lt;br /&gt;
        if (empty(cKOMM_SYSUID)) then begin&lt;br /&gt;
            cKOMM_SYSUID := GetNewId(oDB);&lt;br /&gt;
        end;&lt;br /&gt;
&lt;br /&gt;
        xZeit.qSet(&#039;zk_psnr&#039;       , oReader.Param(&#039;psnr&#039;));&lt;br /&gt;
        xZeit.qSet(&#039;zk_starttime&#039;  , oReader.Param(&#039;uhrzeit&#039;));&lt;br /&gt;
        xZeit.qSet(&#039;zk_startdate&#039;  , oReader.Param(&#039;datum&#039;));&lt;br /&gt;
        xZeit.qSet(&#039;zk_kommentar&#039;  , oReader.Param(&#039;comm&#039;));&lt;br /&gt;
        xZeit.qSet(&#039;zk_messunguid&#039; , cUSER_SYSUID);&lt;br /&gt;
        xZeit.qSet(&#039;sys_uid&#039;       , cKOMM_SYSUID);&lt;br /&gt;
&lt;br /&gt;
        if (not xZeit.SaveData(lNew)) then begin&lt;br /&gt;
            qSQLFree(xZeit);&lt;br /&gt;
            oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Kommentar konnte nicht gespeichert werden&#039;);&lt;br /&gt;
            exit;&lt;br /&gt;
        end;&lt;br /&gt;
        qSQLFree(xZeit);&lt;br /&gt;
&lt;br /&gt;
        // Die UID steht jetzt in einem FELD und nicht mehr als nackter Text im&lt;br /&gt;
        // Koerper. Der Writer kann nur JSON schreiben - und das ist gut so:&lt;br /&gt;
        // eine Antwort, die manchmal &#039;ERROR: ...&#039; und manchmal eine UID war,&lt;br /&gt;
        // liess sich vom Aufrufer nicht unterscheiden. Das Javascript liest&lt;br /&gt;
        // dafuer JSON.parse(...).uid, siehe timer.js.&lt;br /&gt;
        oWriter.Str(&#039;uid&#039;, cUSER_SYSUID);&lt;br /&gt;
&lt;br /&gt;
    end else if (cChoice = &#039;stop&#039;) then begin&lt;br /&gt;
&lt;br /&gt;
        // Jeder Wert aus der Anfrage geht durch DB_SQLVal - auch die, die wie&lt;br /&gt;
        // Zahlen aussehen. Der Kommentar ist Freitext aus dem Browser, und&lt;br /&gt;
        // ohne Quoting beendet ein Apostroph darin das SQL-Statement.&lt;br /&gt;
        cSql := &#039;UPDATE zeiterfuser, zeiterfkomm &#039;   +&lt;br /&gt;
                &#039; SET zeiterfuser.ze_zeit = &#039;        + DB_SQLVal(oReader.Param(&#039;elapsedTime&#039;)) +&lt;br /&gt;
                &#039;, zeiterfuser.ze_endtime = &#039;        + DB_SQLVal(oReader.Param(&#039;vergZeit&#039;)) +&lt;br /&gt;
                &#039;, zeiterfuser.ze_enddate = &#039;        + DB_SQLVal(oReader.Param(&#039;stopTimeValue&#039;)) +&lt;br /&gt;
                &#039;, zeiterfuser.ze_pausezeit = &#039;      + DB_SQLVal(oReader.Param(&#039;elapsedPauseTime&#039;)) +&lt;br /&gt;
                &#039;, zeiterfuser.ze_status = &#039;         + DB_SQLVal(oReader.Param(&#039;status&#039;)) +&lt;br /&gt;
                &#039;, zeiterfkomm.zk_kommentar = &#039;      + DB_SQLVal(oReader.Param(&#039;comment&#039;)) +&lt;br /&gt;
                &#039; WHERE zeiterfuser.sys_uid = &#039;      + DB_SQLVal(oReader.Param(&#039;uid&#039;)) +&lt;br /&gt;
                &#039; AND zeiterfkomm.zk_messunguid = &#039;  + DB_SQLVal(oReader.Param(&#039;uid&#039;));&lt;br /&gt;
&lt;br /&gt;
        if (DB_SQLExec(oDB, cSql)) then begin&lt;br /&gt;
            oWriter.Str(&#039;status&#039;, &#039;ok&#039;);&lt;br /&gt;
        end else begin&lt;br /&gt;
            // Ein Fehlschlag ist ein 500 und keine 200 mit dem Wort &#039;Fehler!&#039;&lt;br /&gt;
            // im Koerper - sonst haelt der Aufrufer ihn fuer einen Erfolg.&lt;br /&gt;
            oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Die Daten konnten nicht gespeichert werden&#039;);&lt;br /&gt;
        end;&lt;br /&gt;
&lt;br /&gt;
    end else if (cChoice = &#039;pause&#039;) then begin&lt;br /&gt;
        writeln(&#039;Pausezeit: &#039; + oReader.Param(&#039;pauseTime&#039;));&lt;br /&gt;
        cSql := &#039;UPDATE zeiterfuser SET ze_pausezeit &#039; +&lt;br /&gt;
                &#039;= ze_pausezeit + &#039;    + DB_SQLVal(oReader.Param(&#039;pauseTime&#039;)) +&lt;br /&gt;
                &#039; WHERE sys_uid = &#039;    + DB_SQLVal(oReader.Param(&#039;sysuid&#039;));&lt;br /&gt;
&lt;br /&gt;
        if (DB_SQLExec(oDB, cSql)) then begin&lt;br /&gt;
            oWriter.Str(&#039;status&#039;, &#039;ok&#039;);&lt;br /&gt;
        end else begin&lt;br /&gt;
            oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Die Pausenzeit konnte nicht gespeichert werden&#039;);&lt;br /&gt;
        end;&lt;br /&gt;
&lt;br /&gt;
    end else begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Unbekannte Auswahl: &#039; + cChoice);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
= HTML-Datei index.php =&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;de-DE&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;head&amp;gt;&lt;br /&gt;
        &amp;lt;title&amp;gt;Ihr IT-Partner in Stade - Ernst Bergau GmbH&amp;lt;/title&amp;gt;&lt;br /&gt;
        &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; type=&amp;quot;text/css&amp;quot; href=&amp;quot;timestyle.css&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;/head&amp;gt;&lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        &amp;lt;?php&lt;br /&gt;
            include(&#039;timer.js&#039;);&lt;br /&gt;
        ?&amp;gt;&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
    &amp;lt;body&amp;gt;&lt;br /&gt;
        &amp;lt;!-- //--------------------------------------------------------------------------------------------------------------// --&amp;gt;&lt;br /&gt;
        &amp;lt;!-- //---------------------------------------------------HTML-------------------------------------------------------// --&amp;gt;&lt;br /&gt;
        &amp;lt;!-- //--------------------------------------------------------------------------------------------------------------// --&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;textarea id=&amp;quot;search&amp;quot; type=&amp;quot;text&amp;quot; rows=&amp;quot;1&amp;quot; cols=&amp;quot;50&amp;quot; oninput=&amp;quot;searchCustomers(this.value)&amp;quot;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;div class=&amp;quot;kundenliste&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;ul id=&amp;quot;kundenListe&amp;quot;&amp;gt;&amp;lt;/ul&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;table class=&amp;quot;timer&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;tbody id=&amp;quot;timerList&amp;quot;&amp;gt;&amp;lt;/tbody&amp;gt;&lt;br /&gt;
        &amp;lt;/table&amp;gt;&lt;br /&gt;
        &amp;lt;table class=&amp;quot;tabelleUeberschriften&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;thead&amp;gt;&lt;br /&gt;
                &amp;lt;tr&amp;gt;&lt;br /&gt;
                    &amp;lt;th class=&amp;quot;kunde&amp;quot;&amp;gt;Kunde&amp;lt;/th&amp;gt;&lt;br /&gt;
                    &amp;lt;th class=&amp;quot;konto&amp;quot;&amp;gt;Konto&amp;lt;/th&amp;gt;&lt;br /&gt;
                    &amp;lt;th class=&amp;quot;laufzeit&amp;quot;&amp;gt;Laufzeit&amp;lt;/th&amp;gt;&lt;br /&gt;
                    &amp;lt;th class=&amp;quot;status&amp;quot;&amp;gt;Status&amp;lt;/th&amp;gt;&lt;br /&gt;
                &amp;lt;/tr&amp;gt;&lt;br /&gt;
            &amp;lt;/thead&amp;gt;&lt;br /&gt;
        &amp;lt;/table&amp;gt;&lt;br /&gt;
        &lt;br /&gt;
&lt;br /&gt;
        &amp;lt;div class=&amp;quot;lightbox&amp;quot; id=&amp;quot;lightbox&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;label class=&amp;quot;commLabel&amp;quot; id=&amp;quot;commLabel&amp;quot; for=&amp;quot;comment&amp;quot;&amp;gt;Kommentar&amp;lt;/label&amp;gt;&lt;br /&gt;
            &amp;lt;textarea class=&amp;quot;comment&amp;quot; id=&amp;quot;comment&amp;quot; rows=&amp;quot;4&amp;quot; cols=&amp;quot;50&amp;quot;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;br /&gt;
&lt;br /&gt;
            &amp;lt;div class=&amp;quot;bezZeitkonto&amp;quot;&amp;gt;Zeitkonto&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;select class=&amp;quot;dropButtonsLightbox&amp;quot; id=&amp;quot;tätigkeit2Dropdown&amp;quot;&amp;gt;&amp;lt;/select&amp;gt;&lt;br /&gt;
&lt;br /&gt;
            &amp;lt;div class=&amp;quot;bezDatum&amp;quot;&amp;gt;Startdatum&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;input type=&amp;quot;date&amp;quot; class=&amp;quot;date&amp;quot; id=&amp;quot;date&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
            &amp;lt;textarea disabled class=&amp;quot;uhrzeit&amp;quot; id=&amp;quot;uhrzeit&amp;quot;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;br /&gt;
&lt;br /&gt;
            &amp;lt;div class=&amp;quot;bezPerson&amp;quot;&amp;gt;Person&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;textarea disabled class=&amp;quot;eingPerson&amp;quot; id=&amp;quot;eingPerson&amp;quot;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;br /&gt;
            &amp;lt;textarea disabled class=&amp;quot;eingPersonZusatz&amp;quot; id=&amp;quot;eingPersonZusatz&amp;quot;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;br /&gt;
&lt;br /&gt;
            &amp;lt;div class=&amp;quot;Std&amp;quot;&amp;gt;Std&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;input type=&amp;quot;number&amp;quot; class=&amp;quot;outStd&amp;quot; id=&amp;quot;outStd&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;div class=&amp;quot;Min&amp;quot;&amp;gt;Min&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;input type=&amp;quot;number&amp;quot; class=&amp;quot;outMin&amp;quot; id=&amp;quot;outMin&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;div class=&amp;quot;Sek&amp;quot;&amp;gt;Sek&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;input type=&amp;quot;number&amp;quot; class=&amp;quot;outSek&amp;quot; id=&amp;quot;outSek&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
            &amp;lt;div class=&amp;quot;Pausenzeiten&amp;quot;&amp;gt;Davon Pausenzeiten&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;div class=&amp;quot;Std2&amp;quot;&amp;gt;Std&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;input type=&amp;quot;number&amp;quot; class=&amp;quot;outStd2&amp;quot; id=&amp;quot;outStd2&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;div class=&amp;quot;Min2&amp;quot;&amp;gt;Min&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;input type=&amp;quot;number&amp;quot; class=&amp;quot;outMin2&amp;quot; id=&amp;quot;outMin2&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;div class=&amp;quot;Sek2&amp;quot;&amp;gt;Sek&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;input type=&amp;quot;number&amp;quot; class=&amp;quot;outSek2&amp;quot; id=&amp;quot;outSek2&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
            &amp;lt;button class=&amp;quot;saveButton&amp;quot; id=&amp;quot;saveButton&amp;quot;&amp;gt;Speichern&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;button class=&amp;quot;closeButton&amp;quot; id=&amp;quot;closeButton&amp;quot; onclick=&amp;quot;closeLightboxOnSchliessen()&amp;quot;&amp;gt;Schließen&amp;lt;/button&amp;gt; &lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
        &lt;br /&gt;
        &amp;lt;div id=&amp;quot;zweiteListe&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;ul id=&amp;quot;taetigkeitListe&amp;quot;&amp;gt;&amp;lt;/ul&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
= Javascript-Datei timer.js =&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;javascript&amp;quot;&amp;gt;&lt;br /&gt;
var api_key             = &#039;Y00C75PODJ&#039;;&lt;br /&gt;
var url                 = &#039;http://127.0.0.1:8099/Zeiterfa/zeiterfa/v1&#039;;&lt;br /&gt;
var zaehler             = -1;&lt;br /&gt;
var timerInterval;      &lt;br /&gt;
var pauseTimerInterval; &lt;br /&gt;
var startTimeValue;     &lt;br /&gt;
var selectedID          = 0;&lt;br /&gt;
var elapsedTime;        &lt;br /&gt;
var uid;                &lt;br /&gt;
var idStop; &lt;br /&gt;
var time;&lt;br /&gt;
var selID               = [];&lt;br /&gt;
var job                 = [];&lt;br /&gt;
var sys_uid             = [];&lt;br /&gt;
var timers              = [];    &lt;br /&gt;
var pauseTimers         = [];  &lt;br /&gt;
var pauseTime           = [];&lt;br /&gt;
var startdate           = [];&lt;br /&gt;
var uhrzeit             = [];&lt;br /&gt;
var datum               = [];&lt;br /&gt;
var statusTimer         = [];       &lt;br /&gt;
var selectedJob         = [];&lt;br /&gt;
var selectedKunde       = [];      &lt;br /&gt;
var tempSelectedKunde   = [];&lt;br /&gt;
var parameter           = [];&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//--------------------------------------------------Seitenaufruf---------------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Bei Seitenaufruf werden die Funktionen----//&lt;br /&gt;
//----getCustomer und checkTimer aufgerufen und----//&lt;br /&gt;
//----die Kundenliste gefüllt und geprüft,----//&lt;br /&gt;
//----ob laufende timer vorhanden sind----//&lt;br /&gt;
&lt;br /&gt;
window.onload = function() {&lt;br /&gt;
&lt;br /&gt;
    getCustomer();&lt;br /&gt;
    checkTimer();&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//-------------------------------------------------Daten auslesen--------------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Eine Liste der Kunden wird ausgelesenund gespeichert----//&lt;br /&gt;
&lt;br /&gt;
function getCustomer() {&lt;br /&gt;
&lt;br /&gt;
    var method = &#039;POST&#039;;&lt;br /&gt;
    var choice = &#039;customer&#039;;&lt;br /&gt;
&lt;br /&gt;
    var params =    &amp;quot;api_key=&amp;quot;              + encodeURIComponent(api_key) +&lt;br /&gt;
                    &amp;quot;&amp;amp;url=&amp;quot;                 + encodeURIComponent(url) +&lt;br /&gt;
                    &amp;quot;&amp;amp;method=&amp;quot;              + encodeURIComponent(method) +&lt;br /&gt;
                    &amp;quot;&amp;amp;choice=&amp;quot;              + encodeURIComponent(choice);&lt;br /&gt;
&lt;br /&gt;
    var request = new XMLHttpRequest();&lt;br /&gt;
&lt;br /&gt;
    request.open(&amp;quot;POST&amp;quot;, &amp;quot;getData.php&amp;quot;);&lt;br /&gt;
    request.setRequestHeader(&amp;quot;Content-Type&amp;quot;, &amp;quot;application/x-www-form-urlencoded&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
    request.onreadystatechange = function() {&lt;br /&gt;
        if (this.readyState === 4 &amp;amp;&amp;amp; this.status === 200) {&lt;br /&gt;
            var responseData = JSON.parse(request.responseText);&lt;br /&gt;
            populateListeKunde(responseData);&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    request.send(params);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Bei Rechtsklick auf Kunden wird----//&lt;br /&gt;
//----seine ID ausgelesen und gespeichert----//&lt;br /&gt;
&lt;br /&gt;
function getCustomerID(selectedKunde, callback) {&lt;br /&gt;
&lt;br /&gt;
    var method = &#039;POST&#039;;&lt;br /&gt;
    var choice = &#039;id&#039;;&lt;br /&gt;
&lt;br /&gt;
    var params =    &amp;quot;api_key=&amp;quot;              + encodeURIComponent(api_key) +&lt;br /&gt;
                    &amp;quot;&amp;amp;url=&amp;quot;                 + encodeURIComponent(url) +&lt;br /&gt;
                    &amp;quot;&amp;amp;method=&amp;quot;              + encodeURIComponent(method) +&lt;br /&gt;
                    &amp;quot;&amp;amp;choice=&amp;quot;              + encodeURIComponent(choice) +&lt;br /&gt;
                    &amp;quot;&amp;amp;selectedKunde=&amp;quot;       + encodeURIComponent(selectedKunde);&lt;br /&gt;
&lt;br /&gt;
    var request = new XMLHttpRequest();&lt;br /&gt;
&lt;br /&gt;
    request.open(&amp;quot;POST&amp;quot;, &amp;quot;getData.php&amp;quot;);&lt;br /&gt;
    request.setRequestHeader(&amp;quot;Content-Type&amp;quot;, &amp;quot;application/x-www-form-urlencoded&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
    request.onreadystatechange = function() {&lt;br /&gt;
        if (this.readyState === 4 &amp;amp;&amp;amp; this.status === 200) {&lt;br /&gt;
            selectedID = JSON.parse(request.responseText);&lt;br /&gt;
            callback(selectedID);&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    request.send(params);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Anhand der Kunden-ID wird eine Liste von Tätigkeiten----//&lt;br /&gt;
//----für jeden Kunden ausgelesen und gespeichert----//&lt;br /&gt;
&lt;br /&gt;
function getJob(id, callback) {&lt;br /&gt;
&lt;br /&gt;
    var method = &#039;POST&#039;;&lt;br /&gt;
    var choice = &#039;job&#039;;&lt;br /&gt;
&lt;br /&gt;
    var params =    &amp;quot;api_key=&amp;quot;              + encodeURIComponent(api_key) +&lt;br /&gt;
                    &amp;quot;&amp;amp;url=&amp;quot;                 + encodeURIComponent(url) +&lt;br /&gt;
                    &amp;quot;&amp;amp;method=&amp;quot;              + encodeURIComponent(method) +&lt;br /&gt;
                    &amp;quot;&amp;amp;choice=&amp;quot;              + encodeURIComponent(choice) +&lt;br /&gt;
                    &amp;quot;&amp;amp;id=&amp;quot;                  + encodeURIComponent(id);&lt;br /&gt;
&lt;br /&gt;
    var request = new XMLHttpRequest();&lt;br /&gt;
&lt;br /&gt;
    request.open(&amp;quot;POST&amp;quot;, &amp;quot;getData.php&amp;quot;);&lt;br /&gt;
    request.setRequestHeader(&amp;quot;Content-Type&amp;quot;, &amp;quot;application/x-www-form-urlencoded&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
    request.onreadystatechange = function() {&lt;br /&gt;
        if (this.readyState === 4 &amp;amp;&amp;amp; this.status === 200) {&lt;br /&gt;
            var responseData = JSON.parse(request.responseText);&lt;br /&gt;
            var Data         = responseData.toString();&lt;br /&gt;
            var teile        = Data.split(&amp;quot;,&amp;quot;);&lt;br /&gt;
            var jobid = [];&lt;br /&gt;
&lt;br /&gt;
            for (i=0; i&amp;lt;teile.length; i++) {&lt;br /&gt;
                job[i]   = teile[i].split(&amp;quot;:&amp;quot;)[0];&lt;br /&gt;
                jobid[i] = teile[i].split(&amp;quot;:&amp;quot;)[1];&lt;br /&gt;
            }&lt;br /&gt;
            callback(job, jobid);&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    request.send(params);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Tätigkeitsliste wird nochmal zur Weiterverarbeitung gelesen----//&lt;br /&gt;
&lt;br /&gt;
function getJobs(id) {&lt;br /&gt;
&lt;br /&gt;
    var method = &#039;POST&#039;;&lt;br /&gt;
    var choice = &#039;job&#039;;&lt;br /&gt;
&lt;br /&gt;
    var params =    &amp;quot;api_key=&amp;quot;              + encodeURIComponent(api_key) +&lt;br /&gt;
                    &amp;quot;&amp;amp;url=&amp;quot;                 + encodeURIComponent(url) +&lt;br /&gt;
                    &amp;quot;&amp;amp;method=&amp;quot;              + encodeURIComponent(method) +&lt;br /&gt;
                    &amp;quot;&amp;amp;choice=&amp;quot;              + encodeURIComponent(choice) +&lt;br /&gt;
                    &amp;quot;&amp;amp;id=&amp;quot;                  + encodeURIComponent(id);&lt;br /&gt;
&lt;br /&gt;
    var request = new XMLHttpRequest();&lt;br /&gt;
&lt;br /&gt;
    request.open(&amp;quot;POST&amp;quot;, &amp;quot;getData.php&amp;quot;, false);&lt;br /&gt;
    request.setRequestHeader(&amp;quot;Content-Type&amp;quot;, &amp;quot;application/x-www-form-urlencoded&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
    request.onreadystatechange = function() {&lt;br /&gt;
        if (this.readyState === 4 &amp;amp;&amp;amp; this.status === 200) {&lt;br /&gt;
            var responseData = JSON.parse(request.responseText);&lt;br /&gt;
            var data = responseData.toString();&lt;br /&gt;
            var teile = data.split(&amp;quot;,&amp;quot;);&lt;br /&gt;
            var jobid = [];&lt;br /&gt;
&lt;br /&gt;
            for (var i = 0; i &amp;lt; teile.length; i++) {&lt;br /&gt;
                job[i] = teile[i].split(&amp;quot;:&amp;quot;)[0];&lt;br /&gt;
                jobid[i] = teile[i].split(&amp;quot;:&amp;quot;)[1];&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    request.send(params);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
//----Gespeicherte Pausezeit wird ausgelesen----//&lt;br /&gt;
&lt;br /&gt;
function getPauseTime(sysuid, id) {&lt;br /&gt;
&lt;br /&gt;
    var method = &#039;POST&#039;;&lt;br /&gt;
    var choice = &#039;pauseTime&#039;;&lt;br /&gt;
&lt;br /&gt;
    var params  =   &amp;quot;api_key=&amp;quot;              + encodeURIComponent(api_key) +&lt;br /&gt;
                    &amp;quot;&amp;amp;url=&amp;quot;                 + encodeURIComponent(url) +&lt;br /&gt;
                    &amp;quot;&amp;amp;method=&amp;quot;              + encodeURIComponent(method) +&lt;br /&gt;
                    &amp;quot;&amp;amp;choice=&amp;quot;              + encodeURIComponent(choice) +&lt;br /&gt;
                    &amp;quot;&amp;amp;sysuid=&amp;quot;              + encodeURIComponent(sysuid);&lt;br /&gt;
&lt;br /&gt;
    var request = new XMLHttpRequest();&lt;br /&gt;
&lt;br /&gt;
    request.open(&amp;quot;POST&amp;quot;, &amp;quot;getData.php&amp;quot;, false);&lt;br /&gt;
    request.setRequestHeader(&amp;quot;Content-Type&amp;quot;, &amp;quot;application/x-www-form-urlencoded&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
    request.onreadystatechange = function() {&lt;br /&gt;
        if (this.readyState === 4 &amp;amp;&amp;amp; this.status === 200) {&lt;br /&gt;
            pauseTime[id].value = JSON.parse(request.responseText);&lt;br /&gt;
&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    request.send(params);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//---------------------------------------------Listen--------------------------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Eine Liste wird mit Kundendaten gefüllt und angezeigt----//&lt;br /&gt;
&lt;br /&gt;
function populateListeKunde(data) {&lt;br /&gt;
&lt;br /&gt;
    data.forEach(function(item) {&lt;br /&gt;
        var listItem = document.createElement(&amp;quot;li&amp;quot;);&lt;br /&gt;
        listItem.textContent = item;&lt;br /&gt;
&lt;br /&gt;
        listItem.addEventListener(&amp;quot;contextmenu&amp;quot;, function(event) {&lt;br /&gt;
            event.preventDefault();&lt;br /&gt;
&lt;br /&gt;
            var selectedKunde = item;&lt;br /&gt;
            getCustomerID(selectedKunde, function(selectedID) {&lt;br /&gt;
                getJob(selectedID, function(job, jobid) {&lt;br /&gt;
                    showSecondList(job, jobid, item);&lt;br /&gt;
                });&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        kundenListe.appendChild(listItem);&lt;br /&gt;
    });&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Eine zweite Liste wird mit Jobs gefüllt,----//&lt;br /&gt;
//----angezeigt und bei Klick auf einen Eintrag---//&lt;br /&gt;
//-------die Funktion startTimer aufgerufen-------//&lt;br /&gt;
&lt;br /&gt;
function showSecondList(job, jobid, kundeItem) {&lt;br /&gt;
&lt;br /&gt;
    var zweiteListe = document.getElementById(&amp;quot;zweiteListe&amp;quot;);&lt;br /&gt;
    zweiteListe.innerHTML = &amp;quot;&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
    job.forEach(function(item) {&lt;br /&gt;
        var listItem = document.createElement(&amp;quot;li&amp;quot;);&lt;br /&gt;
        listItem.textContent = item;&lt;br /&gt;
    &lt;br /&gt;
        (function(capturedItem) {&lt;br /&gt;
            listItem.addEventListener(&amp;quot;click&amp;quot;, function() {&lt;br /&gt;
                startTimer(job, jobid, capturedItem, kundeItem);&lt;br /&gt;
            });&lt;br /&gt;
        }) (item);&lt;br /&gt;
        zweiteListe.appendChild(listItem);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    zweiteListe.style.display = &amp;quot;block&amp;quot;; &lt;br /&gt;
    document.addEventListener(&#039;keydown&#039;, closeSecondList);&lt;br /&gt;
    document.addEventListener(&#039;click&#039;, closeSecondList);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Nach Klick auf Eintrag wird die Liste geschlossen----//&lt;br /&gt;
function closeSecondList() {&lt;br /&gt;
&lt;br /&gt;
    var zweiteListe = document.getElementById(&amp;quot;zweiteListe&amp;quot;);&lt;br /&gt;
    zweiteListe.style.display = &amp;quot;none&amp;quot;; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//---------------------------------------------Daten speichern-----------------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Startzeit, -datum, ID und Job werden gespeichert----//&lt;br /&gt;
&lt;br /&gt;
function saveStartTime(user, uhrzeit, datum, endTime, endDate, status, psnr, Job, jobID) {&lt;br /&gt;
    &lt;br /&gt;
    var method = &#039;PUT&#039;;&lt;br /&gt;
    var choice = &#039;start&#039;;&lt;br /&gt;
&lt;br /&gt;
    var params =    &amp;quot;api_key=&amp;quot;              + encodeURIComponent(api_key) +&lt;br /&gt;
                    &amp;quot;&amp;amp;url=&amp;quot;                 + encodeURIComponent(url) +&lt;br /&gt;
                    &amp;quot;&amp;amp;method=&amp;quot;              + encodeURIComponent(method) +&lt;br /&gt;
                    &amp;quot;&amp;amp;choice=&amp;quot;              + encodeURIComponent(choice) +&lt;br /&gt;
                    &amp;quot;&amp;amp;user=&amp;quot;                + encodeURIComponent(user) +&lt;br /&gt;
                    &amp;quot;&amp;amp;uhrzeit=&amp;quot;             + encodeURIComponent(uhrzeit) +&lt;br /&gt;
                    &amp;quot;&amp;amp;datum=&amp;quot;               + encodeURIComponent(datum) +&lt;br /&gt;
                    &amp;quot;&amp;amp;endTime=&amp;quot;             + encodeURIComponent(endTime) +&lt;br /&gt;
                    &amp;quot;&amp;amp;endDate=&amp;quot;             + encodeURIComponent(endDate) +&lt;br /&gt;
                    &amp;quot;&amp;amp;status=&amp;quot;              + encodeURIComponent(status)+&lt;br /&gt;
                    &amp;quot;&amp;amp;psnr=&amp;quot;                + encodeURIComponent(psnr)+&lt;br /&gt;
                    &amp;quot;&amp;amp;Job=&amp;quot;                 + encodeURIComponent(Job)+&lt;br /&gt;
                    &amp;quot;&amp;amp;jobID=&amp;quot;               + encodeURIComponent(jobID);&lt;br /&gt;
&lt;br /&gt;
    var request = new XMLHttpRequest();&lt;br /&gt;
&lt;br /&gt;
    request.open(&amp;quot;POST&amp;quot;, &amp;quot;saveTime.php&amp;quot;);&lt;br /&gt;
    request.setRequestHeader(&amp;quot;Content-Type&amp;quot;, &amp;quot;application/x-www-form-urlencoded&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
    request.onreadystatechange = function() {&lt;br /&gt;
        if (this.readyState === 4 &amp;amp;&amp;amp; this.status === 200) {&lt;br /&gt;
            // Die Antwort ist JSON: {&amp;quot;uid&amp;quot;:&amp;quot;...&amp;quot;} - vorher stand die UID als&lt;br /&gt;
            // nackter Text im Koerper.&lt;br /&gt;
            sys_uid[zaehler].value = JSON.parse(request.responseText).uid;&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    request.send(params);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
//----Pausezeit wird gespeichert----//&lt;br /&gt;
&lt;br /&gt;
function savePauseTime(pauseTime, id) {&lt;br /&gt;
&lt;br /&gt;
    var method = &#039;PUT&#039;;&lt;br /&gt;
    var choice = &#039;pause&#039;;&lt;br /&gt;
    &lt;br /&gt;
    var params =    &amp;quot;api_key=&amp;quot;      + encodeURIComponent(api_key) +&lt;br /&gt;
                    &amp;quot;&amp;amp;url=&amp;quot;         + encodeURIComponent(url) +&lt;br /&gt;
                    &amp;quot;&amp;amp;method=&amp;quot;      + encodeURIComponent(method) +&lt;br /&gt;
                    &amp;quot;&amp;amp;choice=&amp;quot;      + encodeURIComponent(choice) +&lt;br /&gt;
                    &amp;quot;&amp;amp;pauseTime=&amp;quot;   + encodeURIComponent(pauseTime) +&lt;br /&gt;
                    &amp;quot;&amp;amp;sysuid=&amp;quot;      + encodeURIComponent(sys_uid[id].value);&lt;br /&gt;
&lt;br /&gt;
    var request = new XMLHttpRequest();&lt;br /&gt;
&lt;br /&gt;
    request.open(&amp;quot;POST&amp;quot;, &amp;quot;saveTime.php&amp;quot;);&lt;br /&gt;
    request.setRequestHeader(&amp;quot;Content-Type&amp;quot;, &amp;quot;application/x-www-form-urlencoded&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
    request.onreadystatechange = function() {&lt;br /&gt;
&lt;br /&gt;
        if (this.readyState === 4 &amp;amp;&amp;amp; this.status === 200) {&lt;br /&gt;
            // alert(request.responseText);&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    request.send(params);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
//----Stopzeit und -datum wird gespeichert----//&lt;br /&gt;
&lt;br /&gt;
function saveStopTime(stopTimeValue, elapsedTime, elapsedPauseTime, status, uid, vergZeit, comment) {&lt;br /&gt;
&lt;br /&gt;
    var method = &#039;PUT&#039;;&lt;br /&gt;
    var choice = &#039;stop&#039;;&lt;br /&gt;
    &lt;br /&gt;
    var params =    &amp;quot;api_key=&amp;quot;              + encodeURIComponent(api_key) +&lt;br /&gt;
                    &amp;quot;&amp;amp;url=&amp;quot;                 + encodeURIComponent(url) +&lt;br /&gt;
                    &amp;quot;&amp;amp;method=&amp;quot;              + encodeURIComponent(method) +&lt;br /&gt;
                    &amp;quot;&amp;amp;choice=&amp;quot;              + encodeURIComponent(choice) +&lt;br /&gt;
                    &amp;quot;&amp;amp;stopTimeValue=&amp;quot;       + encodeURIComponent(stopTimeValue) +&lt;br /&gt;
                    &amp;quot;&amp;amp;elapsedTime=&amp;quot;         + encodeURIComponent(elapsedTime) +&lt;br /&gt;
                    &amp;quot;&amp;amp;elapsedPauseTime=&amp;quot;    + encodeURIComponent(elapsedPauseTime) +&lt;br /&gt;
                    &amp;quot;&amp;amp;status=&amp;quot;              + encodeURIComponent(status) +&lt;br /&gt;
                    &amp;quot;&amp;amp;uid=&amp;quot;                 + encodeURIComponent(uid) +&lt;br /&gt;
                    &amp;quot;&amp;amp;vergZeit=&amp;quot;            + encodeURIComponent(vergZeit)+&lt;br /&gt;
                    &amp;quot;&amp;amp;comment=&amp;quot;            + encodeURIComponent(comment);&lt;br /&gt;
    &lt;br /&gt;
    var request = new XMLHttpRequest();&lt;br /&gt;
&lt;br /&gt;
    request.open(&amp;quot;POST&amp;quot;, &amp;quot;saveTime.php&amp;quot;);&lt;br /&gt;
    request.setRequestHeader(&amp;quot;Content-Type&amp;quot;, &amp;quot;application/x-www-form-urlencoded&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
    request.onreadystatechange = function() {&lt;br /&gt;
        if (this.readyState === 4 &amp;amp;&amp;amp; this.status === 200 &amp;amp;&amp;amp; timers.length === 0) {&lt;br /&gt;
            location.reload();&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    request.send(params);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//---------------------------------------------Like-Suche----------------------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Durchsucht die Liste der Kunden auf alle einegegebenen Zeichen----//&lt;br /&gt;
&lt;br /&gt;
function searchCustomers(text) {&lt;br /&gt;
&lt;br /&gt;
    text = text.toLowerCase();&lt;br /&gt;
    var kundenListe = document.getElementById(&amp;quot;kundenListe&amp;quot;);&lt;br /&gt;
    var listItems = kundenListe.getElementsByTagName(&amp;quot;li&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
    for (i=0; i&amp;lt;listItems.length; i++) {&lt;br /&gt;
        var item = listItems[i].textContent.toLowerCase();&lt;br /&gt;
        if (item.includes(text)) {&lt;br /&gt;
            listItems[i].style.display = &amp;quot;block&amp;quot;;&lt;br /&gt;
        }&lt;br /&gt;
        else {&lt;br /&gt;
            listItems[i].style.display = &amp;quot;none&amp;quot;;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//------------------------------------------Vorhandenen Timer checken----------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Überprüft, ob es Einträge in Datenbank ohne----//&lt;br /&gt;
//----Endzeit gibt und startet entsprechend Timer----//&lt;br /&gt;
&lt;br /&gt;
function checkTimer() {&lt;br /&gt;
&lt;br /&gt;
    var method = &#039;POST&#039;;&lt;br /&gt;
    var choice = &#039;check&#039;;&lt;br /&gt;
&lt;br /&gt;
    var params =    &amp;quot;api_key=&amp;quot;              + encodeURIComponent(api_key) +&lt;br /&gt;
                    &amp;quot;&amp;amp;url=&amp;quot;                 + encodeURIComponent(url) +&lt;br /&gt;
                    &amp;quot;&amp;amp;method=&amp;quot;              + encodeURIComponent(method) +&lt;br /&gt;
                    &amp;quot;&amp;amp;choice=&amp;quot;              + encodeURIComponent(choice);&lt;br /&gt;
&lt;br /&gt;
    var request = new XMLHttpRequest();&lt;br /&gt;
&lt;br /&gt;
    request.open(&amp;quot;POST&amp;quot;, &amp;quot;getData.php&amp;quot;);&lt;br /&gt;
    request.setRequestHeader(&amp;quot;Content-Type&amp;quot;, &amp;quot;application/x-www-form-urlencoded&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
    request.onreadystatechange = function() {&lt;br /&gt;
        if (this.readyState === 4 &amp;amp;&amp;amp; this.status === 200 &amp;amp;&amp;amp; timers.length === 0) {&lt;br /&gt;
            var responseData = JSON.parse(request.responseText);&lt;br /&gt;
            &lt;br /&gt;
            if (responseData.length !== 0) {&lt;br /&gt;
                startTimeValue = new Date();&lt;br /&gt;
                var minutes = startTimeValue.getMinutes();&lt;br /&gt;
                var seconds = startTimeValue.getSeconds();&lt;br /&gt;
                var month = startTimeValue.getMonth() + 1;&lt;br /&gt;
&lt;br /&gt;
                responseData = responseData.toString();&lt;br /&gt;
                var teile = responseData.split(&amp;quot;,&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
                for (var i=0; i&amp;lt;teile.length; i+=6) {&lt;br /&gt;
                    uhrzeit.push({value: startTimeValue.getHours() + &amp;quot;:&amp;quot; + (minutes &amp;lt; 10 ? &amp;quot;0&amp;quot; + minutes : minutes) + &amp;quot;:&amp;quot; + (seconds &amp;lt; 10 ? &amp;quot;0&amp;quot; + seconds : seconds)});&lt;br /&gt;
                    datum.push({value: startTimeValue.getFullYear() + &amp;quot;-&amp;quot; + (month &amp;lt; 10 ? &amp;quot;0&amp;quot; + month : month) + &amp;quot;-&amp;quot; + startTimeValue.getDate()});&lt;br /&gt;
&lt;br /&gt;
                    zaehler++;&lt;br /&gt;
&lt;br /&gt;
                    statusTimer.push({value: &#039;Running&#039;});&lt;br /&gt;
                    pauseTimers.push({value: 0});&lt;br /&gt;
                    pauseTime.push({value: 0});&lt;br /&gt;
                    timers.push({value: teile[i]});&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                var j=0;&lt;br /&gt;
                for (var i=1; i&amp;lt;teile.length; i+=6) {&lt;br /&gt;
                    selID.push({value: teile[i]});&lt;br /&gt;
                    getJobs(selID[j].value);&lt;br /&gt;
                    j++;&lt;br /&gt;
                } &lt;br /&gt;
&lt;br /&gt;
                fillArray(2, selectedJob, teile);&lt;br /&gt;
                fillArray(3, selectedKunde, teile);&lt;br /&gt;
                fillArray(4, sys_uid, teile);&lt;br /&gt;
                fillArray(5, startdate, teile);&lt;br /&gt;
&lt;br /&gt;
                for (var i=0; i&amp;lt;timers.length; i++) {&lt;br /&gt;
                    var startZeitTeil = timers[i].value.split(&amp;quot;:&amp;quot;);&lt;br /&gt;
                    var startDatumTeil = startdate[i].value.split(&amp;quot;.&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
                    var diffMilli = new Date() - getStartdatum(startDatumTeil, startZeitTeil);&lt;br /&gt;
                    timers[i].value = Math.floor(diffMilli / 1000); &lt;br /&gt;
                }   &lt;br /&gt;
&lt;br /&gt;
                selecteID = 0;&lt;br /&gt;
                startInterval(job);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    request.send(params);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Arrays werden an diese Funktion übergeben und gefüllt----//&lt;br /&gt;
&lt;br /&gt;
function fillArray(LZ, array, teile) {&lt;br /&gt;
&lt;br /&gt;
    var arrLZ = 0;&lt;br /&gt;
    for (var i=LZ; i&amp;lt;teile.length; i+=6) {&lt;br /&gt;
        array.push({value: teile[i]});&lt;br /&gt;
        arrLZ++;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Startdatum wird errechnet----//&lt;br /&gt;
function getStartdatum(startDatumTeil, startZeitTeil) {&lt;br /&gt;
&lt;br /&gt;
    var startdatum = new Date(&lt;br /&gt;
        parseInt(startDatumTeil[2]),&lt;br /&gt;
        parseInt(startDatumTeil[1]) -1,&lt;br /&gt;
        parseInt(startDatumTeil[0]),&lt;br /&gt;
        parseInt(startZeitTeil[0]),&lt;br /&gt;
        parseInt(startZeitTeil[1]),&lt;br /&gt;
        parseInt(startZeitTeil[2])&lt;br /&gt;
    );&lt;br /&gt;
    return startdatum;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//-------------------------------------------------Interval--------------------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Interval für Timer wird gestartet----//&lt;br /&gt;
&lt;br /&gt;
function startInterval(job, item, kundeItem, index) {&lt;br /&gt;
&lt;br /&gt;
    timerInterval = setInterval(function() {&lt;br /&gt;
&lt;br /&gt;
        for (i=0; i&amp;lt;timers.length; i++) {&lt;br /&gt;
&lt;br /&gt;
            timers[i].value += 1;&lt;br /&gt;
        }&lt;br /&gt;
        updateUI(job, item, kundeItem, index);&lt;br /&gt;
    }, 1000);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Interval für Pausentimer wird gestartet----// &lt;br /&gt;
&lt;br /&gt;
function pauseInterval(job, item, kundeItem, id) {&lt;br /&gt;
&lt;br /&gt;
    pauseTimerInterval = setInterval(function() {&lt;br /&gt;
&lt;br /&gt;
        for (i=0; i&amp;lt;pauseTimers.length; i++) {&lt;br /&gt;
&lt;br /&gt;
            pauseTimers[i].value += 1;&lt;br /&gt;
        }&lt;br /&gt;
        updateUI(job, item, kundeItem);&lt;br /&gt;
    }, 1000);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//------------------------------------------------Timer starten----------------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----startTimer bereitet die nötigen Arrays vor,----//&lt;br /&gt;
//----speichert Startzeit und -datum,----//&lt;br /&gt;
//----beschreibt Arrays und Variablen entsprechend----//&lt;br /&gt;
//----und ruft die Funktion saveStartTime auf----//&lt;br /&gt;
&lt;br /&gt;
function startTimer(jobs, jobid, selJob, kundeItem) {&lt;br /&gt;
&lt;br /&gt;
    var user = 999;&lt;br /&gt;
    var endTime = &amp;quot;00:00:00&amp;quot;;&lt;br /&gt;
    var endDate = &amp;quot;0000-00-00&amp;quot;;&lt;br /&gt;
    var status = &#039;01&#039;;&lt;br /&gt;
    zaehler++;&lt;br /&gt;
    &lt;br /&gt;
    startTimeValue = new Date();&lt;br /&gt;
&lt;br /&gt;
    var minutes = startTimeValue.getMinutes();&lt;br /&gt;
    var seconds = startTimeValue.getSeconds();&lt;br /&gt;
    uhrzeit.push({value: startTimeValue.getHours() + &amp;quot;:&amp;quot; + (minutes &amp;lt; 10 ? &amp;quot;0&amp;quot; + minutes : minutes) + &amp;quot;:&amp;quot; + (seconds &amp;lt; 10 ? &amp;quot;0&amp;quot; + seconds : seconds)});&lt;br /&gt;
&lt;br /&gt;
    var month = startTimeValue.getMonth() + 1;&lt;br /&gt;
    datum.push({value: startTimeValue.getFullYear() + &amp;quot;-&amp;quot; + (month &amp;lt; 10 ? &amp;quot;0&amp;quot; + month : month) + &amp;quot;-&amp;quot; + startTimeValue.getDate()});&lt;br /&gt;
&lt;br /&gt;
    statusTimer.push({value: &#039;Running&#039;});&lt;br /&gt;
    pauseTimers.push({value: 0});&lt;br /&gt;
    pauseTime.push({value: 0});&lt;br /&gt;
    timers.push({value: 0});&lt;br /&gt;
    selectedKunde.push({value: kundeItem});&lt;br /&gt;
    selectedJob.push({value: selJob});&lt;br /&gt;
    sys_uid.push({value: 0});&lt;br /&gt;
&lt;br /&gt;
    var index = jobs.indexOf(selJob);&lt;br /&gt;
    &lt;br /&gt;
    saveStartTime(user, uhrzeit[zaehler].value, datum[zaehler].value, endTime, endDate, status, selectedID, selJob, jobid[index]);&lt;br /&gt;
    &lt;br /&gt;
    clearInterval(timerInterval);&lt;br /&gt;
    startInterval(jobs, selJob, kundeItem, index);&lt;br /&gt;
&lt;br /&gt;
    zweiteListe.style.display = &amp;quot;none&amp;quot;;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//------------------------------------------------Timer stoppen----------------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Stopt den Timer, ändert den Status und ruft openLightbox auf----//&lt;br /&gt;
&lt;br /&gt;
function stopTimer(job, item, kundeItem, index, id) {&lt;br /&gt;
&lt;br /&gt;
    idStop = id;&lt;br /&gt;
    statusTimer[id].value = &#039;Stopped&#039;;&lt;br /&gt;
    openLightbox(job, item, kundeItem, index, id);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//------------------------------------------------Timer pausieren--------------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Pausiert den Timer und ändert den Status----//&lt;br /&gt;
&lt;br /&gt;
function pauseTimer(job, item, kundeItem, id) {&lt;br /&gt;
&lt;br /&gt;
    pauseTimers[id].value = 0;&lt;br /&gt;
    clearInterval(pauseTimerInterval);&lt;br /&gt;
    pauseInterval(job, item, kundeItem, id);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//----------------------------------------------Button Pause-Start-------------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Ruft die Funktion pauseTimer oder----//&lt;br /&gt;
//----savePauseTimer auf----//&lt;br /&gt;
&lt;br /&gt;
function toggleButton(job, item, kundeItem, id) {&lt;br /&gt;
&lt;br /&gt;
    var button = document.getElementById(id);&lt;br /&gt;
&lt;br /&gt;
    if (button.innerHTML === &amp;quot;Pause&amp;quot;) {&lt;br /&gt;
&lt;br /&gt;
        statusTimer[id].value = &#039;Paused&#039;;&lt;br /&gt;
        pauseTimer(job, item, kundeItem, id);&lt;br /&gt;
    } &lt;br /&gt;
    else {&lt;br /&gt;
&lt;br /&gt;
        statusTimer[id].value = &amp;quot;Running&amp;quot;;&lt;br /&gt;
        savePauseTime(pauseTimers[id].value, id);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//------------------------------------------------Ausgabe des Timers-----------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Zeigt eine Liste mit einem oder mehreren----//&lt;br /&gt;
//----Timern und Buttons, verteilt dynamisch ID&#039;s----//&lt;br /&gt;
//----und ruft entsprechend der gedrückten----//&lt;br /&gt;
//----Buttons Funktionen auf----//&lt;br /&gt;
&lt;br /&gt;
function updateUI(job, item, kundeItem, index) {&lt;br /&gt;
    var timerList = document.getElementById(&amp;quot;timerList&amp;quot;);&lt;br /&gt;
    timerList.innerHTML = &#039;&#039;;&lt;br /&gt;
    &lt;br /&gt;
    for (i=0; i&amp;lt;timers.length; i++) {&lt;br /&gt;
        var neueReihe = document.createElement(&#039;tr&#039;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
        //----Name des Kunden wird ausgegeben----//&lt;br /&gt;
&lt;br /&gt;
        var kundeEintrag = document.createElement(&#039;td&#039;);&lt;br /&gt;
        kundeEintrag.style.position = &amp;quot;fixed&amp;quot;;&lt;br /&gt;
        kundeEintrag.style.top = (12 + i * 14) + &amp;quot;%&amp;quot;;&lt;br /&gt;
        kundeEintrag.style.width = &amp;quot;30%&amp;quot;;&lt;br /&gt;
        kundeEintrag.style.left = &amp;quot;0%&amp;quot;; &lt;br /&gt;
        kundeEintrag.style.textAlign = &amp;quot;center&amp;quot;;&lt;br /&gt;
        kundeEintrag.classList.add(&#039;kunde_k&#039;);&lt;br /&gt;
        kundeEintrag.textContent = selectedKunde[i].value;&lt;br /&gt;
        timerList.appendChild(kundeEintrag);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
        //----Gewählter Job wird ausgegeben----//&lt;br /&gt;
&lt;br /&gt;
        var kontoEintrag = document.createElement(&#039;td&#039;);&lt;br /&gt;
        kontoEintrag.style.position = &amp;quot;fixed&amp;quot;;&lt;br /&gt;
        kontoEintrag.style.top = (12 + i * 14) + &amp;quot;%&amp;quot;; &lt;br /&gt;
        kontoEintrag.style.width = &amp;quot;30%&amp;quot;;&lt;br /&gt;
        kontoEintrag.style.left = &amp;quot;30%&amp;quot;; &lt;br /&gt;
        kontoEintrag.style.textAlign = &amp;quot;center&amp;quot;;&lt;br /&gt;
        kontoEintrag.classList.add(&#039;konto_k&#039;);&lt;br /&gt;
        kontoEintrag.textContent = selectedJob[i].value;&lt;br /&gt;
        timerList.appendChild(kontoEintrag);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
        //----Gestartete Timer werden in Liste----//&lt;br /&gt;
        //----ausgegeben, pausierte Timer werden----//&lt;br /&gt;
        //----Liste ausgegeben, sobald Timer gestoppt----//&lt;br /&gt;
        //----wird Ausgabe in Lightbox----//&lt;br /&gt;
&lt;br /&gt;
        if (statusTimer[i].value === &amp;quot;Running&amp;quot;) {&lt;br /&gt;
            var laufzeitElement = document.createElement(&#039;td&#039;);&lt;br /&gt;
            laufzeitElement.style.position = &amp;quot;fixed&amp;quot;;&lt;br /&gt;
            laufzeitElement.style.top = (12 + i * 14) + &amp;quot;%&amp;quot;; &lt;br /&gt;
            laufzeitElement.style.width = &amp;quot;15%&amp;quot;;&lt;br /&gt;
            laufzeitElement.style.left = &amp;quot;60%&amp;quot;; &lt;br /&gt;
            laufzeitElement.style.textAlign = &amp;quot;center&amp;quot;;&lt;br /&gt;
            laufzeitElement.classList.add(&#039;laufzeit_l&#039;);&lt;br /&gt;
            laufzeitElement.textContent = formatElapsedTime(timers[i].value);&lt;br /&gt;
            timerList.appendChild(laufzeitElement);&lt;br /&gt;
        }&lt;br /&gt;
        else if (statusTimer[i].value === &#039;Paused&#039;) {&lt;br /&gt;
            var laufzeitElement = document.createElement(&#039;td&#039;);&lt;br /&gt;
            laufzeitElement.style.position = &amp;quot;fixed&amp;quot;;&lt;br /&gt;
            laufzeitElement.style.top = (12 + i * 14) + &amp;quot;%&amp;quot;; &lt;br /&gt;
            laufzeitElement.style.width = &amp;quot;15%&amp;quot;;&lt;br /&gt;
            laufzeitElement.style.left = &amp;quot;60%&amp;quot;; &lt;br /&gt;
            laufzeitElement.style.textAlign = &amp;quot;center&amp;quot;;&lt;br /&gt;
            laufzeitElement.classList.add(&#039;laufzeit_l&#039;);&lt;br /&gt;
            laufzeitElement.textContent = formatElapsedTime(pauseTimers[i].value);&lt;br /&gt;
            timerList.appendChild(laufzeitElement);&lt;br /&gt;
        }&lt;br /&gt;
        else if (statusTimer[i].value === &#039;Stopped&#039;) {&lt;br /&gt;
            var laufzeitElement = document.createElement(&#039;td&#039;);&lt;br /&gt;
            laufzeitElement.style.position = &amp;quot;fixed&amp;quot;;&lt;br /&gt;
            laufzeitElement.style.top = (12 + i * 14) + &amp;quot;%&amp;quot;; &lt;br /&gt;
            laufzeitElement.style.width = &amp;quot;15%&amp;quot;;&lt;br /&gt;
            laufzeitElement.style.left = &amp;quot;60%&amp;quot;; &lt;br /&gt;
            laufzeitElement.style.textAlign = &amp;quot;center&amp;quot;;&lt;br /&gt;
            laufzeitElement.classList.add(&#039;laufzeit_l&#039;);&lt;br /&gt;
            laufzeitElement.textContent = formatElapsedTime(pauseTimers[i].value);&lt;br /&gt;
            timerList.appendChild(laufzeitElement);&lt;br /&gt;
&lt;br /&gt;
            var zeit = formatElapsedTime(timers[i].value);&lt;br /&gt;
            zeit = zeit.toString();&lt;br /&gt;
            var teile = zeit.split(&amp;quot;:&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
            var h = teile[0];&lt;br /&gt;
            var m = teile[1];&lt;br /&gt;
            var s = teile[2];&lt;br /&gt;
&lt;br /&gt;
            var outStdField = document.getElementById(&amp;quot;outStd&amp;quot;);&lt;br /&gt;
            var outMinField = document.getElementById(&amp;quot;outMin&amp;quot;);&lt;br /&gt;
            var outSekField = document.getElementById(&amp;quot;outSek&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
            if (document.activeElement !== outStdField &amp;amp;&amp;amp; document.activeElement !== outMinField &amp;amp;&amp;amp; document.activeElement !== outSekField) {&lt;br /&gt;
                document.getElementById(&amp;quot;outStd&amp;quot;).value = h; &lt;br /&gt;
                document.getElementById(&amp;quot;outMin&amp;quot;).value = m;&lt;br /&gt;
                document.getElementById(&amp;quot;outSek&amp;quot;).value = s;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            var hEnd = document.getElementById(&amp;quot;outStd&amp;quot;).value;&lt;br /&gt;
            var mEnd = document.getElementById(&amp;quot;outMin&amp;quot;).value;&lt;br /&gt;
            var sEnd = document.getElementById(&amp;quot;outSek&amp;quot;).value;&lt;br /&gt;
            time = hEnd + &amp;quot;:&amp;quot; + mEnd + &amp;quot;:&amp;quot; + sEnd;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
        //----Status (Running, Paused, Stopped) wird ausgegeben----//&lt;br /&gt;
&lt;br /&gt;
        var statusTimerElement = document.createElement(&#039;td&#039;);&lt;br /&gt;
        statusTimerElement.style.position = &amp;quot;fixed&amp;quot;;&lt;br /&gt;
        statusTimerElement.style.top = (12 + i * 14) + &amp;quot;%&amp;quot;; &lt;br /&gt;
        statusTimerElement.style.width = &amp;quot;15%&amp;quot;;&lt;br /&gt;
        statusTimerElement.style.left = &amp;quot;75%&amp;quot;; &lt;br /&gt;
        statusTimerElement.style.textAlign = &amp;quot;center&amp;quot;;&lt;br /&gt;
        statusTimerElement.classList.add(&#039;status_s&#039;);&lt;br /&gt;
        statusTimerElement.textContent = statusTimer[i].value;&lt;br /&gt;
        timerList.appendChild(statusTimerElement);&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
        //----Pause Button mit dynamisch vergebener ID wird ausgegeben----//&lt;br /&gt;
&lt;br /&gt;
        if (statusTimer[i].value === &amp;quot;Running&amp;quot;) {&lt;br /&gt;
            var pauseButton = document.createElement(&#039;button&#039;)&lt;br /&gt;
            pauseButton.id = i;&lt;br /&gt;
            pauseButton.classList.add(&#039;pauseButton&#039;);&lt;br /&gt;
            pauseButton.style.position = &amp;quot;fixed&amp;quot;;&lt;br /&gt;
            pauseButton.style.top = (12 + i * 14) + &amp;quot;%&amp;quot;;&lt;br /&gt;
            pauseButton.style.left = &amp;quot;90%&amp;quot;; &lt;br /&gt;
            pauseButton.style.height = &amp;quot;9.4%&amp;quot;;&lt;br /&gt;
            pauseButton.style.minWidth = &amp;quot;3%&amp;quot;;&lt;br /&gt;
            pauseButton.style.fontSize = &amp;quot;80%&amp;quot;;&lt;br /&gt;
            pauseButton.textContent = &amp;quot;Pause&amp;quot;&lt;br /&gt;
            pauseButton.addEventListener(&#039;click&#039;, function() {toggleButton(job, item, kundeItem, this.id)});&lt;br /&gt;
            timerList.appendChild(pauseButton);&lt;br /&gt;
        }&lt;br /&gt;
        else if (statusTimer[i].value === &amp;quot;Paused&amp;quot;) {&lt;br /&gt;
            var pauseButton = document.createElement(&#039;button&#039;)&lt;br /&gt;
            pauseButton.id = i;&lt;br /&gt;
            pauseButton.classList.add(&#039;pauseButton&#039;);&lt;br /&gt;
            pauseButton.style.position = &amp;quot;fixed&amp;quot;;&lt;br /&gt;
            pauseButton.style.top = (12 + i * 14) + &amp;quot;%&amp;quot;;&lt;br /&gt;
            pauseButton.style.left = &amp;quot;90%&amp;quot;;&lt;br /&gt;
            pauseButton.style.height = &amp;quot;9.4%&amp;quot;;&lt;br /&gt;
            pauseButton.style.minWidth = &amp;quot;3%&amp;quot;;&lt;br /&gt;
            pauseButton.style.fontSize = &amp;quot;80%&amp;quot;;&lt;br /&gt;
            pauseButton.textContent = &amp;quot;Start&amp;quot;&lt;br /&gt;
            pauseButton.addEventListener(&#039;click&#039;, function() {toggleButton(job, item, kundeItem, this.id)});&lt;br /&gt;
            timerList.appendChild(pauseButton);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
        //----Stop Button mit dynamisch vergebener ID wird ausgegeben----//&lt;br /&gt;
&lt;br /&gt;
        var stopButton = document.createElement(&#039;button&#039;)&lt;br /&gt;
        stopButton.id = i;&lt;br /&gt;
        stopButton.classList.add(&#039;stopButton&#039;);&lt;br /&gt;
        stopButton.style.position = &amp;quot;fixed&amp;quot;;&lt;br /&gt;
        stopButton.style.top = (12 + i * 14) + &amp;quot;%&amp;quot;;&lt;br /&gt;
        stopButton.style.left = &amp;quot;94%&amp;quot;;&lt;br /&gt;
        stopButton.style.height = &amp;quot;9.4%&amp;quot;;&lt;br /&gt;
        stopButton.style.width = &amp;quot;3%&amp;quot;;&lt;br /&gt;
        stopButton.style.fontSize = &amp;quot;80%&amp;quot;;&lt;br /&gt;
        stopButton.textContent = &amp;quot;Stop&amp;quot;&lt;br /&gt;
        stopButton.addEventListener(&#039;click&#039;, function() {stopTimer(job, item, kundeItem, index, this.id)});&lt;br /&gt;
        timerList.appendChild(stopButton);&lt;br /&gt;
        &lt;br /&gt;
        //----Für jeden aufgerufenen Timer wird----//&lt;br /&gt;
        //----eine neue Reihe ausgegeben----//&lt;br /&gt;
&lt;br /&gt;
        timerList.appendChild(neueReihe);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------Sekunden in Uhrzeit umrechnen-------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Rechnet übergebene Sekunden in Stunden,----//&lt;br /&gt;
//----Minuten und Sekunden und gibt diese----//&lt;br /&gt;
//----an aufrufende Funktion zurück----//&lt;br /&gt;
&lt;br /&gt;
function formatElapsedTime(seconds) {&lt;br /&gt;
&lt;br /&gt;
    var minutes = Math.floor(seconds / 60);&lt;br /&gt;
    seconds %= 60;&lt;br /&gt;
    var hours = Math.floor(minutes / 60);&lt;br /&gt;
    minutes %= 60;&lt;br /&gt;
    function addLeadingZero(n) {&lt;br /&gt;
        return (n &amp;lt; 10 ? &#039;0&#039; : &#039;&#039;) + n;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return addLeadingZero(hours) + &amp;quot;:&amp;quot; + addLeadingZero(minutes) + &amp;quot;:&amp;quot; + addLeadingZero(seconds);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//--------------------------------------------Uhrzeit in Sekunden umrechnen----------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Rechnet übergebene Stunden, Minuten----//&lt;br /&gt;
//----und Sekunden in Sekunden um und----//&lt;br /&gt;
//----gibt diese an aufrufende Funktion zurück----//&lt;br /&gt;
&lt;br /&gt;
function timeInSeconds(zeit) {&lt;br /&gt;
&lt;br /&gt;
    var teile = zeit.split(&#039;:&#039;);&lt;br /&gt;
    var Stunden = parseInt(teile[0], 10);&lt;br /&gt;
    var Minuten = parseInt(teile[1], 10);&lt;br /&gt;
    var Sekunden = parseInt(teile[2], 10);&lt;br /&gt;
    return Stunden * 3600 + Minuten * 60 + Sekunden;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
//--------------------------------------------------Lightbox-------------------------------------------------------------//&lt;br /&gt;
//-----------------------------------------------------------------------------------------------------------------------//&lt;br /&gt;
&lt;br /&gt;
//----Öffnet eine Lightbox und zeigt Kunde----//&lt;br /&gt;
//----Job, vergangene Zeit und evtl Pausenzeit an----//&lt;br /&gt;
//----und gibt Möglichkeit alle Zeiten anzupassen----//&lt;br /&gt;
&lt;br /&gt;
function openLightbox(job, item, kundeItem, index, id) {&lt;br /&gt;
&lt;br /&gt;
    document.getElementById(&#039;lightbox&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
&lt;br /&gt;
    var dateField = document.getElementById(&amp;quot;date&amp;quot;);&lt;br /&gt;
    dateField.value = datum[id].value;&lt;br /&gt;
    document.getElementById(&amp;quot;uhrzeit&amp;quot;).innerHTML = uhrzeit[id].value;&lt;br /&gt;
&lt;br /&gt;
    var commentTextarea = document.getElementById(&amp;quot;comment&amp;quot;);&lt;br /&gt;
    commentTextarea.value = selectedJob[id].value;&lt;br /&gt;
&lt;br /&gt;
    document.getElementById(&amp;quot;eingPersonZusatz&amp;quot;).innerHTML = selectedKunde[id].value;&lt;br /&gt;
&lt;br /&gt;
    if (selectedID.length !== undefined) {&lt;br /&gt;
        document.getElementById(&amp;quot;eingPerson&amp;quot;).innerHTML = selectedID;&lt;br /&gt;
    }&lt;br /&gt;
    else {&lt;br /&gt;
        document.getElementById(&amp;quot;eingPerson&amp;quot;).innerHTML = selID[id].value;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    //----Ausgabe der vergangenen Zeit in Lightbox----//&lt;br /&gt;
&lt;br /&gt;
    document.getElementById(&amp;quot;outStd&amp;quot;).value = 00;                      &lt;br /&gt;
    document.getElementById(&amp;quot;outMin&amp;quot;).value = 00;                      &lt;br /&gt;
    document.getElementById(&amp;quot;outSek&amp;quot;).value = 00;                      &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    //----Ausgabe der Pausenzeiten in Lightbox----//&lt;br /&gt;
&lt;br /&gt;
    document.getElementById(&amp;quot;outStd2&amp;quot;).value = 00;                      &lt;br /&gt;
    document.getElementById(&amp;quot;outMin2&amp;quot;).value = 00;                      &lt;br /&gt;
    document.getElementById(&amp;quot;outSek2&amp;quot;).value = 00;                           &lt;br /&gt;
    getPauseTime(sys_uid[id].value, id);                                &lt;br /&gt;
                                                                        &lt;br /&gt;
    var secondsPause = pauseTime[id].value;                             &lt;br /&gt;
    secondsPause %= 60;  &lt;br /&gt;
&lt;br /&gt;
    var minutesPause = Math.floor(pauseTime[id].value / 60);            &lt;br /&gt;
    minutesPause %= 60;    &lt;br /&gt;
&lt;br /&gt;
    var hoursPause = Math.floor(minutesPause / 60);                     &lt;br /&gt;
    hoursPause %= 60;   &lt;br /&gt;
&lt;br /&gt;
    document.getElementById(&amp;quot;outStd2&amp;quot;).value = hoursPause;              &lt;br /&gt;
    document.getElementById(&amp;quot;outMin2&amp;quot;).value = minutesPause;       &lt;br /&gt;
&lt;br /&gt;
    if (secondsPause &amp;gt; 0) {&lt;br /&gt;
        document.getElementById(&amp;quot;outSek2&amp;quot;).value = secondsPause;        &lt;br /&gt;
    }                                                                   &lt;br /&gt;
    else {&lt;br /&gt;
        document.getElementById(&amp;quot;outSek2&amp;quot;).value = 0;                   &lt;br /&gt;
    }                                                                   &lt;br /&gt;
&lt;br /&gt;
    var dropdown2 = document.getElementById(&amp;quot;tätigkeit2Dropdown&amp;quot;);&lt;br /&gt;
    &lt;br /&gt;
    job.forEach(function(item, index) {&lt;br /&gt;
        var option = document.createElement(&amp;quot;option&amp;quot;);&lt;br /&gt;
        option.textContent = item;&lt;br /&gt;
        dropdown2.add(option)&lt;br /&gt;
&lt;br /&gt;
        if (item === selectedJob[id].value) {&lt;br /&gt;
            option.selected = true;&lt;br /&gt;
        }&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    document.addEventListener(&#039;keydown&#039;, closeLightboxOnEscape);&lt;br /&gt;
    document.addEventListener(&#039;keydown&#039;, closeLightboxOnF2);&lt;br /&gt;
}  &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Lightbox schließen----//&lt;br /&gt;
&lt;br /&gt;
function closeLightbox() {&lt;br /&gt;
&lt;br /&gt;
    document.getElementById(&#039;lightbox&#039;).style.display = &#039;none&#039;;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Funktion closeLightbox wird aufgerufen,----//&lt;br /&gt;
//----wenn der Schließen-Button gedrückt wird----//&lt;br /&gt;
&lt;br /&gt;
function closeLightboxOnSchliessen() {&lt;br /&gt;
&lt;br /&gt;
    statusTimer[idStop].value = &#039;Running&#039;;&lt;br /&gt;
    closeLightbox();&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Funktion closeLightbox wird aufgerufen,----//&lt;br /&gt;
//----wenn Escape gedrückt wird----//&lt;br /&gt;
&lt;br /&gt;
function closeLightboxOnEscape() {&lt;br /&gt;
&lt;br /&gt;
    if (event.key === &#039;Escape&#039;) {&lt;br /&gt;
        statusTimer[idStop].value = &#039;Running&#039;;&lt;br /&gt;
        closeLightbox();&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Funktion closeLightbox wird aufgerufen,----//&lt;br /&gt;
//----wenn F2 gedrückt wird----//&lt;br /&gt;
&lt;br /&gt;
function closeLightboxOnF2(event) {&lt;br /&gt;
&lt;br /&gt;
    if (event.key === &#039;F2&#039;) {&lt;br /&gt;
        saveBox();&lt;br /&gt;
        closeLightbox();&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Alle Daten in Lightbox werden gespeichert----//&lt;br /&gt;
&lt;br /&gt;
function saveBox() {&lt;br /&gt;
&lt;br /&gt;
    var vergZeit = &#039;&amp;quot;&#039; + time + &#039;&amp;quot;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var teile = time.split(&amp;quot;:&amp;quot;);&lt;br /&gt;
    var stunden = parseInt(teile[0]);&lt;br /&gt;
    var minuten = parseInt(teile[1]);&lt;br /&gt;
    var sekunden = parseInt(teile[2]);&lt;br /&gt;
    time = stunden * 3600 + minuten * 60 + sekunden;&lt;br /&gt;
&lt;br /&gt;
    elapsedTime = time;&lt;br /&gt;
&lt;br /&gt;
    var comment = &#039;&amp;quot;&#039; + document.getElementById(&amp;quot;comment&amp;quot;).value + &#039;&amp;quot;&#039;;&lt;br /&gt;
    &lt;br /&gt;
    var hPEnd = document.getElementById(&amp;quot;outStd2&amp;quot;).value;&lt;br /&gt;
    var mPEnd = document.getElementById(&amp;quot;outMin2&amp;quot;).value;&lt;br /&gt;
    var sPEnd = document.getElementById(&amp;quot;outSek2&amp;quot;).value;&lt;br /&gt;
    var pTime = hPEnd + &amp;quot;:&amp;quot; + mPEnd + &amp;quot;:&amp;quot; + sPEnd;&lt;br /&gt;
    pTime = timeInSeconds(pTime);&lt;br /&gt;
&lt;br /&gt;
    var uid = sys_uid[idStop].value;&lt;br /&gt;
    var month = startTimeValue.getMonth();&lt;br /&gt;
    var day = startTimeValue.getDate();&lt;br /&gt;
    month += 1;&lt;br /&gt;
    var stopTimeValue = &#039;&amp;quot;&#039; + startTimeValue.getFullYear() + &amp;quot;-&amp;quot; + (month &amp;lt; 10 ? &amp;quot;0&amp;quot; + month : month) + &amp;quot;-&amp;quot; + (day &amp;lt; 10 ? &amp;quot;0&amp;quot; + day : day) + &#039;&amp;quot;&#039;;&lt;br /&gt;
    &lt;br /&gt;
    var status = 21;&lt;br /&gt;
&lt;br /&gt;
    timers.splice(idStop, 1);&lt;br /&gt;
    pauseTimers.splice(idStop, 1);&lt;br /&gt;
    pauseTime.splice(idStop, 1);&lt;br /&gt;
    selectedKunde.splice(idStop, 1);&lt;br /&gt;
    selectedJob.splice(idStop, 1);&lt;br /&gt;
    statusTimer[idStop].value = &#039;Running&#039;;&lt;br /&gt;
    sys_uid.splice(idStop, 1);&lt;br /&gt;
&lt;br /&gt;
    if (zaehler &amp;gt; 0) {&lt;br /&gt;
        zaehler -= 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    closeLightbox();&lt;br /&gt;
&lt;br /&gt;
    if (timers.length === 0) {&lt;br /&gt;
        clearInterval(timerInterval);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    saveStopTime(stopTimeValue, elapsedTime, pTime, status, uid, vergZeit, comment);&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
//----Funktion saveBox wird aufgerufen,----//&lt;br /&gt;
//----wenn der Save-Button gedrückt wird----//&lt;br /&gt;
&lt;br /&gt;
document.addEventListener(&#039;DOMContentLoaded&#039;, function() {&lt;br /&gt;
        const saveButton = document.getElementById(&#039;saveButton&#039;);&lt;br /&gt;
        saveButton.addEventListener(&#039;click&#039;, function() {&lt;br /&gt;
            saveBox();&lt;br /&gt;
        })&lt;br /&gt;
});&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
= PHP-Script saveTime.php =&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
    $user               = 0;&lt;br /&gt;
    $uhrzeit            = 0;&lt;br /&gt;
    $datum              = 0;&lt;br /&gt;
    $endTime            = 0;&lt;br /&gt;
    $endDate            = 0;&lt;br /&gt;
    $status             = 0;&lt;br /&gt;
    $psnr               = 0;&lt;br /&gt;
    $comm               = 0;&lt;br /&gt;
    $jobID              = 0;&lt;br /&gt;
    $pauseTime          = 0;&lt;br /&gt;
    $sysuid             = 0;&lt;br /&gt;
    $stopTimeValue      = 0;&lt;br /&gt;
    $elapsedTime        = 0;&lt;br /&gt;
    $elapsedPauseTime   = 0;&lt;br /&gt;
    $status             = 0;&lt;br /&gt;
    $uid                = 0;&lt;br /&gt;
    $vergZeit           = 0;&lt;br /&gt;
&lt;br /&gt;
    if ($_SERVER[&#039;REQUEST_METHOD&#039;] === &#039;POST&#039;) &lt;br /&gt;
    {&lt;br /&gt;
        $api_key            = $_POST[&#039;api_key&#039;];&lt;br /&gt;
        $url                = $_POST[&#039;url&#039;];&lt;br /&gt;
        $method             = $_POST[&#039;method&#039;];&lt;br /&gt;
        $choice             = $_POST[&#039;choice&#039;];&lt;br /&gt;
&lt;br /&gt;
        if (isset($_POST[&#039;user&#039;])) {&lt;br /&gt;
            $user               = $_POST[&#039;user&#039;];&lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;uhrzeit&#039;])) {&lt;br /&gt;
            $uhrzeit            = $_POST[&#039;uhrzeit&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;datum&#039;])) {&lt;br /&gt;
            $datum              = $_POST[&#039;datum&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;endTime&#039;])) {&lt;br /&gt;
            $endTime            = $_POST[&#039;endTime&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;endDate&#039;])) {&lt;br /&gt;
            $endDate            = $_POST[&#039;endDate&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;status&#039;])) {&lt;br /&gt;
            $status             = $_POST[&#039;status&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;psnr&#039;])) {&lt;br /&gt;
            $psnr               = $_POST[&#039;psnr&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;Job&#039;])) {&lt;br /&gt;
            $comm               = $_POST[&#039;Job&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;jobID&#039;])) {&lt;br /&gt;
            $jobID              = $_POST[&#039;jobID&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;pauseTime&#039;])) {&lt;br /&gt;
            $pauseTime          = $_POST[&#039;pauseTime&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;sysuid&#039;])) {&lt;br /&gt;
            $sysuid             = $_POST[&#039;sysuid&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;stopTimeValue&#039;])) {&lt;br /&gt;
            $stopTimeValue      = $_POST[&#039;stopTimeValue&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;elapsedTime&#039;])) {&lt;br /&gt;
            $elapsedTime        = $_POST[&#039;elapsedTime&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;elapsedPauseTime&#039;])) {&lt;br /&gt;
            $elapsedPauseTime   = $_POST[&#039;elapsedPauseTime&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;status&#039;])) {&lt;br /&gt;
            $status             = $_POST[&#039;status&#039;];    &lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;uid&#039;])) {&lt;br /&gt;
            $uid                = $_POST[&#039;uid&#039;];&lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;vergZeit&#039;])) {&lt;br /&gt;
            $vergZeit           = $_POST[&#039;vergZeit&#039;];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $params = array(    &#039;user&#039;              =&amp;gt; $user, &lt;br /&gt;
                            &#039;uhrzeit&#039;           =&amp;gt; $uhrzeit, &lt;br /&gt;
                            &#039;datum&#039;             =&amp;gt; $datum, &lt;br /&gt;
                            &#039;endTime&#039;           =&amp;gt; $endTime, &lt;br /&gt;
                            &#039;endDate&#039;           =&amp;gt; $endDate, &lt;br /&gt;
                            &#039;status&#039;            =&amp;gt; $status, &lt;br /&gt;
                            &#039;psnr&#039;              =&amp;gt; $psnr,&lt;br /&gt;
                            &#039;comm&#039;              =&amp;gt; $comm,&lt;br /&gt;
                            &#039;jobID&#039;             =&amp;gt; $jobID,&lt;br /&gt;
                            &#039;pauseTime&#039;         =&amp;gt; $pauseTime,&lt;br /&gt;
                            &#039;sysuid&#039;            =&amp;gt; $sysuid,&lt;br /&gt;
                            &#039;stopTimeValue&#039;     =&amp;gt; $stopTimeValue,&lt;br /&gt;
                            &#039;elapsedTime&#039;       =&amp;gt; $elapsedTime,&lt;br /&gt;
                            &#039;elapsedPauseTime&#039;  =&amp;gt; $elapsedPauseTime,&lt;br /&gt;
                            &#039;status&#039;            =&amp;gt; $status,&lt;br /&gt;
                            &#039;uid&#039;               =&amp;gt; $uid,&lt;br /&gt;
                            &#039;vergZeit&#039;          =&amp;gt; $vergZeit,&lt;br /&gt;
                            &#039;choice&#039;            =&amp;gt; $choice);&lt;br /&gt;
&lt;br /&gt;
        $curl = curl_init($url);&lt;br /&gt;
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);&lt;br /&gt;
        curl_setopt($curl, CURLOPT_HTTPHEADER, array(&#039;apikey: &#039;.$api_key));&lt;br /&gt;
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);&lt;br /&gt;
        curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($params));&lt;br /&gt;
        $response = curl_exec($curl);&lt;br /&gt;
&lt;br /&gt;
        if ($response === false) {&lt;br /&gt;
            if($error) {&lt;br /&gt;
                echo &amp;quot;curl_Fehler: &amp;quot;.$error;&lt;br /&gt;
            }&lt;br /&gt;
        } &lt;br /&gt;
        else {&lt;br /&gt;
            echo $response;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
?&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
= PHP-Script getData.php =&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
    $sysuid             = 0;&lt;br /&gt;
    $id                 = 0;&lt;br /&gt;
    $selectedKunde      = 0;&lt;br /&gt;
            &lt;br /&gt;
    if ($_SERVER[&#039;REQUEST_METHOD&#039;] === &#039;POST&#039;) &lt;br /&gt;
    {&lt;br /&gt;
        $api_key        = $_POST[&#039;api_key&#039;];&lt;br /&gt;
        $url            = $_POST[&#039;url&#039;];&lt;br /&gt;
        $method         = $_POST[&#039;method&#039;];&lt;br /&gt;
        $choice         = $_POST[&#039;choice&#039;];&lt;br /&gt;
        &lt;br /&gt;
        if (isset($_POST[&#039;id&#039;])) {&lt;br /&gt;
            $id = $_POST[&#039;id&#039;];&lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;sysuid&#039;])) {&lt;br /&gt;
            $sysuid = $_POST[&#039;sysuid&#039;];&lt;br /&gt;
        }&lt;br /&gt;
        if (isset($_POST[&#039;selectedKunde&#039;])) {&lt;br /&gt;
            $selectedKunde  = $_POST[&#039;selectedKunde&#039;];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $params = array(    &#039;sysuid&#039;            =&amp;gt; $sysuid,&lt;br /&gt;
                            &#039;id&#039;                =&amp;gt; $id, &lt;br /&gt;
                            &#039;selectedKunde&#039;     =&amp;gt; $selectedKunde,&lt;br /&gt;
                            &#039;choice&#039;            =&amp;gt; $choice);&lt;br /&gt;
        &lt;br /&gt;
        $curl = curl_init($url);&lt;br /&gt;
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);&lt;br /&gt;
        curl_setopt($curl, CURLOPT_HTTPHEADER, array(&#039;apikey: &#039;.$api_key));&lt;br /&gt;
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);&lt;br /&gt;
        curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($params));&lt;br /&gt;
        $response = curl_exec($curl);&lt;br /&gt;
&lt;br /&gt;
        if ($response === false) {&lt;br /&gt;
            if($error) {&lt;br /&gt;
                echo &amp;quot;curl_Fehler: &amp;quot;.$error;&lt;br /&gt;
            }&lt;br /&gt;
        } &lt;br /&gt;
        else {&lt;br /&gt;
            echo $response;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
?&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
= CSS-Datei timestyle.css =&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;css&amp;quot;&amp;gt;&lt;br /&gt;
/*-------------------------------------------------------------------------------------------------*/&lt;br /&gt;
/*-----------------------------------------Allgemein-----------------------------------------------*/&lt;br /&gt;
/*-------------------------------------------------------------------------------------------------*/&lt;br /&gt;
&lt;br /&gt;
body &lt;br /&gt;
{&lt;br /&gt;
    font-family: &#039;Arial&#039;, sans-serif;&lt;br /&gt;
    display: flex;&lt;br /&gt;
    justify-content: right;&lt;br /&gt;
    align-items: flex-start;&lt;br /&gt;
    height: 100%;&lt;br /&gt;
    background-image: url(&#039;web_background_3.png&#039;);&lt;br /&gt;
    background-repeat: no-repeat;&lt;br /&gt;
    background-size: cover; &lt;br /&gt;
    background-attachment: fixed;&lt;br /&gt;
}&lt;br /&gt;
ul &lt;br /&gt;
{&lt;br /&gt;
    list-style-type: none;&lt;br /&gt;
}&lt;br /&gt;
li &lt;br /&gt;
{&lt;br /&gt;
    padding: 0.5em;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    display: flex; &lt;br /&gt;
    justify-content: space-between;&lt;br /&gt;
    align-items: center;&lt;br /&gt;
    font-size: 0.85em;&lt;br /&gt;
} &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
/*-------------------------------------------------------------------------------------------------*/&lt;br /&gt;
/*-------------------------------------------Suche-------------------------------------------------*/&lt;br /&gt;
/*-------------------------------------------------------------------------------------------------*/&lt;br /&gt;
&lt;br /&gt;
#search&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 8%;&lt;br /&gt;
    left: 42%;&lt;br /&gt;
    width: 15%;&lt;br /&gt;
    height: 2.8%;&lt;br /&gt;
    resize: none;&lt;br /&gt;
    border: 0.04em solid #e4e4e4;&lt;br /&gt;
    border-radius: 0.1em;&lt;br /&gt;
    background-color: white;&lt;br /&gt;
    font-size: 2em;&lt;br /&gt;
    overflow: hidden;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
/*-------------------------------------------------------------------------------------------------*/&lt;br /&gt;
/*-----------------------------------------Timerausgabe--------------------------------------------*/&lt;br /&gt;
/*-------------------------------------------------------------------------------------------------*/&lt;br /&gt;
&lt;br /&gt;
.kundenliste&lt;br /&gt;
{&lt;br /&gt;
    width: 70%;&lt;br /&gt;
    height: 30%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 43.5%;&lt;br /&gt;
    left: 50%;&lt;br /&gt;
    transform: translate(-50%, -50%);&lt;br /&gt;
    border-top-left-radius: 0.2em;&lt;br /&gt;
    border-top-right-radius: 0.2em;&lt;br /&gt;
    overflow: auto;&lt;br /&gt;
    background-color: white;&lt;br /&gt;
    font-size: 0.85vw;&lt;br /&gt;
}&lt;br /&gt;
.tabelleUeberschriften&lt;br /&gt;
{&lt;br /&gt;
    width: 70%;&lt;br /&gt;
    min-height: 3%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 60%;&lt;br /&gt;
    left: 50%;&lt;br /&gt;
    transform: translate(-50%, -50%);&lt;br /&gt;
    /* border-top: 0.14em solid #ccc;&lt;br /&gt;
    border-bottom: 0.14em solid #ccc; */&lt;br /&gt;
    background-color: white;&lt;br /&gt;
    box-shadow: 0 0px 3px rgba(0, 0, 0, 0.5);&lt;br /&gt;
    font-size: 0.9vw;&lt;br /&gt;
}&lt;br /&gt;
#timerList&lt;br /&gt;
{&lt;br /&gt;
    width: 70%;&lt;br /&gt;
    height: 25%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 73.5%;&lt;br /&gt;
    left: 50%;&lt;br /&gt;
    transform: translate(-50%, -50%);&lt;br /&gt;
    overflow: auto;&lt;br /&gt;
    border-bottom-left-radius: 0.2em;&lt;br /&gt;
    border-bottom-right-radius: 0.2em;&lt;br /&gt;
    /* border: 0.14em solid red; */&lt;br /&gt;
    background-color: white;&lt;br /&gt;
    font-size: 0.85vw;&lt;br /&gt;
} &lt;br /&gt;
.kunde&lt;br /&gt;
{&lt;br /&gt;
    width: 30%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    left: 0%;&lt;br /&gt;
}&lt;br /&gt;
.konto&lt;br /&gt;
{&lt;br /&gt;
    width: 30%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    left: 30%;&lt;br /&gt;
}&lt;br /&gt;
.laufzeit&lt;br /&gt;
{&lt;br /&gt;
    width: 15%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    left: 60%;&lt;br /&gt;
}&lt;br /&gt;
.status&lt;br /&gt;
{&lt;br /&gt;
    width: 15%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    left: 75%;&lt;br /&gt;
}&lt;br /&gt;
.buttons&lt;br /&gt;
{&lt;br /&gt;
    width: 10%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    left: 90%;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/*-------------------------------------------------------------------------------------------------*/&lt;br /&gt;
/*-------------------------------------------Lightbox----------------------------------------------*/&lt;br /&gt;
/*-------------------------------------------------------------------------------------------------*/&lt;br /&gt;
&lt;br /&gt;
.lightbox&lt;br /&gt;
{&lt;br /&gt;
    display: none;&lt;br /&gt;
    width: 52%;&lt;br /&gt;
    height: 62%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 61.5%;&lt;br /&gt;
    left: 67%;&lt;br /&gt;
    border-radius: 0.15em;&lt;br /&gt;
    background-color: white;&lt;br /&gt;
    transform: translate(-50%, -50%);&lt;br /&gt;
    flex-direction: column;&lt;br /&gt;
    align-items: center;&lt;br /&gt;
    box-shadow: 0 10px 2000px rgba(0, 0, 0, 0.5);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
/*-----------------------------------------Lightbox-Inhalt-----------------------------------------*/&lt;br /&gt;
&lt;br /&gt;
.commLabel&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 5%;&lt;br /&gt;
    left: 5%;   &lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    font-size: 0.8vw;&lt;br /&gt;
}&lt;br /&gt;
.comment&lt;br /&gt;
{&lt;br /&gt;
    width: 90%;&lt;br /&gt;
    height: 38%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 10%;&lt;br /&gt;
    left: 5%;&lt;br /&gt;
    resize: none;&lt;br /&gt;
    border: 0.1em solid black;&lt;br /&gt;
    border-radius: 0.2em;&lt;br /&gt;
    font-size: 0.9vw;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
.bezZeitkonto&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 55%;&lt;br /&gt;
    left: 5%;&lt;br /&gt;
    font-size: 0.8vw;&lt;br /&gt;
}&lt;br /&gt;
.dropButtonsLightbox&lt;br /&gt;
{&lt;br /&gt;
    width: 37%;&lt;br /&gt;
    height: 4.4%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 54%;&lt;br /&gt;
    left: 15%;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.bezDatum&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 55%;&lt;br /&gt;
    left: 58%;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.date&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 54%;&lt;br /&gt;
    left: 68%;&lt;br /&gt;
    width: 15%;&lt;br /&gt;
    height: 4%;&lt;br /&gt;
    border: 0.1em solid black;&lt;br /&gt;
    border-radius: 0.2em;&lt;br /&gt;
    font-size: 0.8vw;&lt;br /&gt;
}&lt;br /&gt;
.uhrzeit&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 54%;&lt;br /&gt;
    left: 85.5%;&lt;br /&gt;
    width: 10%;&lt;br /&gt;
    height: 3.9%;&lt;br /&gt;
    padding: 0.1em;&lt;br /&gt;
    border: 0.1em solid black;&lt;br /&gt;
    border-radius: 0.2em;&lt;br /&gt;
    font-size: 0.8vw;&lt;br /&gt;
    color: gray;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    resize: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
.bezPerson&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 68%;&lt;br /&gt;
    left: 5%;&lt;br /&gt;
    font-size: 0.8vw;&lt;br /&gt;
}&lt;br /&gt;
.eingPerson&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 68%;&lt;br /&gt;
    left: 15%;&lt;br /&gt;
    width: 10%;&lt;br /&gt;
    height: 4.4%;&lt;br /&gt;
    resize: none;&lt;br /&gt;
    border: 0.1em solid black;&lt;br /&gt;
    border-radius: 0.2em;&lt;br /&gt;
    font-size: 0.8vw;&lt;br /&gt;
    color: gray;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
.eingPersonZusatz&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 68%;&lt;br /&gt;
    left: 26%;&lt;br /&gt;
    width: 25.7%;&lt;br /&gt;
    height: 4.4%;&lt;br /&gt;
    overflow: auto;&lt;br /&gt;
    resize: none;&lt;br /&gt;
    border: 0.1em solid black;&lt;br /&gt;
    border-radius: 0.2em;&lt;br /&gt;
    font-size: 0.8vw;&lt;br /&gt;
    color: gray;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
.Std&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 63%;&lt;br /&gt;
    left: 66%;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.Min&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 63%;&lt;br /&gt;
    left: 78%;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.Sek&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 63%;&lt;br /&gt;
    left: 91%;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.outStd&lt;br /&gt;
{&lt;br /&gt;
    width: 6%;&lt;br /&gt;
    height: 4.4%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 68%;&lt;br /&gt;
    left: 64%;&lt;br /&gt;
    border: 0.1em solid black;&lt;br /&gt;
    border-radius: 0.2em;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.outMin&lt;br /&gt;
{&lt;br /&gt;
    width: 6%;&lt;br /&gt;
    height: 4.4%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 68%;&lt;br /&gt;
    left: 76.5%;&lt;br /&gt;
    border: 0.1em solid black;&lt;br /&gt;
    border-radius: 0.2em;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.outSek&lt;br /&gt;
{&lt;br /&gt;
    width: 6%;&lt;br /&gt;
    height: 4.4%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 68%;&lt;br /&gt;
    left: 89%;&lt;br /&gt;
    border: 0.1em solid black;&lt;br /&gt;
    border-radius: 0.2em;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
.Pausenzeiten&lt;br /&gt;
{&lt;br /&gt;
    position: absolute;&lt;br /&gt;
    top: 79%;&lt;br /&gt;
    left: 71%;&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    font-size: 0.8vw;&lt;br /&gt;
}&lt;br /&gt;
.Std2&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 84%;&lt;br /&gt;
    left: 66%;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.Min2&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 84%;&lt;br /&gt;
    left: 78%;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.Sek2&lt;br /&gt;
{&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 84%;&lt;br /&gt;
    left: 91%;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.outStd2&lt;br /&gt;
{&lt;br /&gt;
    width: 6%;&lt;br /&gt;
    height: 4.4%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 89%;&lt;br /&gt;
    left: 64%;&lt;br /&gt;
    border: 0.1em solid black;&lt;br /&gt;
    border-radius: 0.2em;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.outMin2&lt;br /&gt;
{&lt;br /&gt;
    width: 6%;&lt;br /&gt;
    height: 4.4%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 89%;&lt;br /&gt;
    left: 76.5%;&lt;br /&gt;
    border: 0.1em  solid black;&lt;br /&gt;
    border-radius: 0.2em;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.outSek2&lt;br /&gt;
{&lt;br /&gt;
    width: 6%;&lt;br /&gt;
    height: 4.4%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 89%;&lt;br /&gt;
    left: 89%;&lt;br /&gt;
    border: 0.1em  solid black;&lt;br /&gt;
    border-radius: 0.2em;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.saveButton&lt;br /&gt;
{&lt;br /&gt;
    min-width: 8%;&lt;br /&gt;
    height: 4.4%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 89%;&lt;br /&gt;
    left: 5%;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
.closeButton&lt;br /&gt;
{&lt;br /&gt;
    min-width: 8%;&lt;br /&gt;
    height: 4.4%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 89%;&lt;br /&gt;
    left: 14%;&lt;br /&gt;
    font-size: 0.7vw;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
/*-------------------------------------------------------------------------------------------------*/&lt;br /&gt;
/*--------------------------------Tätigkeitenliste (zweite Liste)----------------------------------*/&lt;br /&gt;
/*-------------------------------------------------------------------------------------------------*/&lt;br /&gt;
&lt;br /&gt;
#zweiteListe &lt;br /&gt;
{&lt;br /&gt;
    display: none;&lt;br /&gt;
    width: 25%;&lt;br /&gt;
    max-height: 50%;&lt;br /&gt;
    position: fixed;&lt;br /&gt;
    top: 47%;&lt;br /&gt;
    left: 44%;&lt;br /&gt;
    padding: 1%;&lt;br /&gt;
    overflow: auto;&lt;br /&gt;
    border-radius: 0.2em;&lt;br /&gt;
    transform: translate(-50%, -50%);&lt;br /&gt;
    background-color: #FAFAFA;&lt;br /&gt;
    box-shadow: 0 10px 2000px rgba(0, 0, 0, 0.5);&lt;br /&gt;
    font-size: 0.85vw;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel1&amp;diff=67349</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Beispiel1</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel1&amp;diff=67349"/>
		<updated>2026-11-05T12:39:53Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Navigationsvorlage mit Vorlage:-Praefix [Volltext ersetzt] [Styleguide-Abweichung: Seitenkopf der Altseite]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Beispiel 1: Daten-Abruf mit JWT-Authentifizierung=&lt;br /&gt;
&lt;br /&gt;
In diesem Beispiel werden Steuerungs-Variablen aus OBS abgerufen und auf einer Webseite ausgegeben. Der Zugang ist mit JWT-Pflicht eingerichtet, der Konsument muss sich daher zuerst anmelden und einen Token bezogen haben.&lt;br /&gt;
&lt;br /&gt;
==Einrichtung in OBS==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Server-Profil:&#039;&#039;&#039; Standard-TLS-Profil &#039;&#039;Public-API&#039;&#039; mit Bindung 0.0.0.0:443&lt;br /&gt;
* &#039;&#039;&#039;Zugang:&#039;&#039;&#039; &#039;&#039;Web-Dashboard&#039;&#039;&lt;br /&gt;
** API-Key: zufällig generiert&lt;br /&gt;
** JWT aktiv, JWT-Endpunkt &#039;&#039;oauth&#039;&#039;, JWT-Key zufällig, JWT-Exp 60 (Minuten)&lt;br /&gt;
** CORS-Origins: &#039;&#039;https://dashboard.firma.de&#039;&#039;&lt;br /&gt;
* &#039;&#039;&#039;Endpunkt:&#039;&#039;&#039; &#039;&#039;steuerung/v1&#039;&#039;, dem Profil &#039;&#039;Public-API&#039;&#039; zugeordnet&lt;br /&gt;
* &#039;&#039;&#039;Berechtigung:&#039;&#039;&#039; Zugang &#039;&#039;Web-Dashboard&#039;&#039; für Endpunkt &#039;&#039;steuerung/v1&#039;&#039; freigeschaltet&lt;br /&gt;
&lt;br /&gt;
==JWT-Authentifizierungs-Skript (Zugang)==&lt;br /&gt;
&lt;br /&gt;
Wird über die Zugänge-Liste mit &#039;&#039;&#039;F7&#039;&#039;&#039; geöffnet. Prüfen, ob Benutzername/Passwort gegen die OBS-Benutzerverwaltung passen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Authenticate(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUser : string;&lt;br /&gt;
    cPass : string;&lt;br /&gt;
    cSql  : string;&lt;br /&gt;
    qUser : TxFQuery;&lt;br /&gt;
    lOk   : Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    cUser := &#039;&#039;;&lt;br /&gt;
    cPass := &#039;&#039;;&lt;br /&gt;
    oReader.Str(&#039;username&#039;, cUser);&lt;br /&gt;
    oReader.Str(&#039;password&#039;, cPass);&lt;br /&gt;
&lt;br /&gt;
    cSql := &#039;SELECT u_nr, u_name FROM benutzer&#039; +&lt;br /&gt;
            &#039; WHERE u_login = &#039; + DB_SQLVal(cUser) +&lt;br /&gt;
            &#039; AND u_passwort_hash = &#039; + DB_SQLVal(HashPasswort(cPass)) +&lt;br /&gt;
            &#039; AND u_aktiv = &#039; + DB_SQLVal(&#039;1&#039;);&lt;br /&gt;
&lt;br /&gt;
    // DB_SOpen liefert true, wenn die ABFRAGE lief - nicht, wenn ein&lt;br /&gt;
    // Datensatz gefunden wurde. Ohne die EoF-Pruefung wuerde jede&lt;br /&gt;
    // Anmeldung gelingen, sobald das SQL fehlerfrei ist.&lt;br /&gt;
    lOk := false;&lt;br /&gt;
    if (DB_SOpen(oDB, cSql, qUser)) then begin&lt;br /&gt;
        if (not qUser.EoF) then begin&lt;br /&gt;
            lOk := true;&lt;br /&gt;
            oWriter.Int(&#039;status&#039;           , 1);&lt;br /&gt;
            oWriter.Str(&#039;_OBS_JWT_ID&#039;      , qUser.A2C(&#039;u_nr&#039;));&lt;br /&gt;
            oWriter.Str(&#039;_OBS_JWT_SUBJECT&#039; , qUser.A2C(&#039;u_name&#039;));&lt;br /&gt;
            oWriter.Str(&#039;_OBS_JWT_AUDIENCE&#039;, &#039;dashboard&#039;);&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
    DB_Close(qUser);&lt;br /&gt;
&lt;br /&gt;
    if (not lOk) then begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;, 9);&lt;br /&gt;
        oWriter.Str(&#039;error&#039; , &#039;Benutzer oder Passwort ist falsch&#039;);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Hier steht in &#039;&#039;_OBS_JWT_ID&#039;&#039; die Benutzernummer - derselbe Wert also bei&lt;br /&gt;
jeder Anmeldung desselben Benutzers. Das ist zulässig: der Server führt seine&lt;br /&gt;
Sitzungen über eigene Kennungen (&#039;&#039;sid&#039;&#039;/&#039;&#039;rid&#039;&#039;), nicht über das &#039;&#039;jti&#039;&#039; des&lt;br /&gt;
Skripts. Mehrfache und parallele Anmeldungen eines Benutzers sind damit&lt;br /&gt;
unproblematisch.}}&lt;br /&gt;
&lt;br /&gt;
==Endpunkt-Skript &#039;&#039;steuerung/v1&#039;&#039;==&lt;br /&gt;
&lt;br /&gt;
Liefert eine Liste von Steuerungs-Werten zurück. Die Auswertung der JWT-Claims sorgt dafür, dass nur Audience &#039;&#039;dashboard&#039;&#039; Daten erhält.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
// Ein Wert als Listeneintrag. Der Helfer bekommt den WRITER und schreibt an&lt;br /&gt;
// der Stelle hinein, an der er aufgerufen wird - er gibt kein Fragment zurueck.&lt;br /&gt;
// Der leere Feldname ist der Eintrag einer Liste.&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure _AddVar(oWriter: TxRestWriter; const cVar: string);&lt;br /&gt;
var cTitel : string;&lt;br /&gt;
    cData  : string;&lt;br /&gt;
    cEinh  : string;&lt;br /&gt;
    lString: Boolean;&lt;br /&gt;
    lAlign : Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    if (ST_Variable(oDB, cVar, cTitel, cData, cEinh, lString, lAlign)) then begin&lt;br /&gt;
        oWriter.ObjBegin(&#039;&#039;);&lt;br /&gt;
            oWriter.Str(&#039;variable&#039;, cVar);&lt;br /&gt;
            oWriter.Str(&#039;titel&#039;   , cTitel);&lt;br /&gt;
            oWriter.Str(&#039;wert&#039;    , cData);&lt;br /&gt;
            oWriter.Str(&#039;einheit&#039; , cEinh);&lt;br /&gt;
        oWriter.ObjEnd;&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    // Audience-Pruefung: nur Dashboard-Tokens akzeptieren. Die Ablehnung ist&lt;br /&gt;
    // ein 403 und keine 200 mit Fehlertext - oWriter.Error baut dieselbe&lt;br /&gt;
    // Huelle, die auch die Server-eigenen Fehler tragen.&lt;br /&gt;
    if (oReader.Param(&#039;_OBS_JWT_AUDIENCE&#039;) &amp;lt;&amp;gt; &#039;dashboard&#039;) then begin&lt;br /&gt;
        oWriter.Error(403, &#039;FORBIDDEN_ROLE&#039;, &#039;Token nicht für diese Anwendung ausgestellt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // Die Antwort ist eine nackte Liste, kein Objekt: RootArr sagt das, und&lt;br /&gt;
    // zwar VOR dem ersten Eintrag. Ein ArrEnd gibt es dazu nicht - die&lt;br /&gt;
    // Wurzelklammer setzt der Server.&lt;br /&gt;
    oWriter.RootArr();&lt;br /&gt;
    _AddVar(oWriter, &#039;WERT_1&#039;);&lt;br /&gt;
    _AddVar(oWriter, &#039;WERT_2&#039;);&lt;br /&gt;
    _AddVar(oWriter, &#039;WERT_3&#039;);&lt;br /&gt;
    _AddVar(oWriter, &#039;WERT_4&#039;);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==JavaScript-Client (Browser)==&lt;br /&gt;
&lt;br /&gt;
Im Browser werden zwei Aufrufe gemacht: erst Token holen, dann Daten lesen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;javascript&amp;quot; line&amp;gt;&lt;br /&gt;
const API_BASE = &#039;https://api.meinserver.de&#039;;&lt;br /&gt;
const API_KEY  = &#039;[API-KEY]&#039;;&lt;br /&gt;
&lt;br /&gt;
async function login(username, password) {&lt;br /&gt;
    const res = await fetch(`${API_BASE}/oauth/`, {&lt;br /&gt;
        method:  &#039;POST&#039;,&lt;br /&gt;
        headers: { &#039;Content-Type&#039;: &#039;application/json&#039;, &#039;apikey&#039;: API_KEY },&lt;br /&gt;
        body:    JSON.stringify({ username, password })&lt;br /&gt;
    });&lt;br /&gt;
    if (!res.ok) throw new Error(&#039;Login fehlgeschlagen&#039;);&lt;br /&gt;
    const data = await res.json();&lt;br /&gt;
    return data.token;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function ladeSteuerung(token) {&lt;br /&gt;
    const res = await fetch(`${API_BASE}/steuerung/v1`, {&lt;br /&gt;
        method:  &#039;GET&#039;,&lt;br /&gt;
        headers: {&lt;br /&gt;
            &#039;apikey&#039;:        API_KEY,&lt;br /&gt;
            &#039;Authorization&#039;: &#039;Bearer &#039; + token&lt;br /&gt;
        }&lt;br /&gt;
    });&lt;br /&gt;
    if (!res.ok) throw new Error(&#039;Abruf fehlgeschlagen: &#039; + res.status);&lt;br /&gt;
    return res.json();&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function zeigeTabelle() {&lt;br /&gt;
    const token = await login(&#039;mitarbeiter&#039;, &#039;geheim&#039;);&lt;br /&gt;
    const werte = await ladeSteuerung(token);&lt;br /&gt;
&lt;br /&gt;
    const tbl = document.getElementById(&#039;steuerung&#039;);&lt;br /&gt;
    werte.forEach(w =&amp;gt; {&lt;br /&gt;
        const tr = document.createElement(&#039;tr&#039;);&lt;br /&gt;
        tr.innerHTML = `&amp;lt;td&amp;gt;${w.titel}&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;${w.wert} ${w.einheit}&amp;lt;/td&amp;gt;`;&lt;br /&gt;
        tbl.appendChild(tr);&lt;br /&gt;
    });&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
zeigeTabelle();&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Test mit curl==&lt;br /&gt;
&lt;br /&gt;
Token holen:&lt;br /&gt;
&lt;br /&gt;
 curl -X POST -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Content-Type: application/json&amp;quot; ^&lt;br /&gt;
      -d &amp;quot;{\&amp;quot;username\&amp;quot;:\&amp;quot;mitarbeiter\&amp;quot;,\&amp;quot;password\&amp;quot;:\&amp;quot;geheim\&amp;quot;}&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/oauth/&lt;br /&gt;
&lt;br /&gt;
Antwort:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;token&amp;quot;:       &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;accessToken&amp;quot;: &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;:   3600,&lt;br /&gt;
  &amp;quot;serverTime&amp;quot;:  &amp;quot;2026-08-19T09:12:33+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Da das Skript kein &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039; liefert, enthält die Antwort kein&lt;br /&gt;
&#039;&#039;refreshToken&#039;&#039; - der Client meldet sich nach Ablauf der 60 Minuten neu an.&lt;br /&gt;
&lt;br /&gt;
Daten abrufen:&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Authorization: Bearer eyJhbGciOi...&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/steuerung/v1&lt;br /&gt;
&lt;br /&gt;
==Was zeigt das Beispiel?==&lt;br /&gt;
&lt;br /&gt;
* Zweistufige Anmeldung: API-Key + JWT.&lt;br /&gt;
* Sichere Trennung von Konsumenten-Gruppen über die Audience-Claim.&lt;br /&gt;
* Eigenes Authentifizierungs-Skript pro Zugang.&lt;br /&gt;
* Browser-Zugriff über CORS-erlaubtes Origin.&lt;br /&gt;
* Anmeldung ohne Refresh-Token: einfachster Fall, der Token läuft nach der Zeit aus &#039;&#039;JWT-Exp&#039;&#039; ab.&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer&amp;diff=67351</id>
		<title>OBS/Kostenpflichtige Module/RESTServer</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer&amp;diff=67351"/>
		<updated>2026-11-05T12:39:52Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Navigationsvorlage mit Vorlage:-Praefix [Volltext ersetzt] [Styleguide-Abweichung: Seitenkopf der Altseite]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
=REST-Server=&lt;br /&gt;
{{Hinweis|Es handelt sich um ein kostenpflichtiges Modul. Die Schnittstelle muss über den OBS-Support aktiviert werden.}}&lt;br /&gt;
&lt;br /&gt;
==Was leistet der REST-Server?==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server ist die &#039;&#039;&#039;universelle Schnittstelle&#039;&#039;&#039; Ihrer OBS-Installation nach außen. Er macht aus Ihrem OBS einen Dienst, mit dem andere Programme - Webseiten, Apps, Geräte, Kundensysteme, Cloud-Dienste - direkt sprechen können. Statt Daten manuell zu exportieren, zu mailen oder über Umwege bereitzustellen, holen sich angebundene Systeme genau die Information, die sie brauchen, in dem Moment, in dem sie sie brauchen - oder liefern neue Daten direkt in OBS ab.&lt;br /&gt;
&lt;br /&gt;
Für Anwender, die nicht selbst programmieren, bedeutet das: &#039;&#039;&#039;Was bisher nur manuell, per Datei-Import oder über Spezialschnittstellen ging, lässt sich jetzt automatisieren.&#039;&#039;&#039; Eine Webseite zeigt live aktuelle Lagerbestände. Mitarbeiter erfassen Zeiten über das Handy, ohne dass jemand Excel-Listen einpflegen muss. Ein Kunde stößt per Bestelltaste in seinem System einen Vorgang in Ihrem OBS an. Ein Lieferant meldet Wareneingänge automatisch zurück. Jeder dieser Anwendungsfälle wird einmal eingerichtet und läuft danach automatisch.&lt;br /&gt;
&lt;br /&gt;
Technisch gesehen ist der REST-Server ein in OBS integrierter, voll konfigurierbarer &#039;&#039;&#039;HTTP/HTTPS-Dienst&#039;&#039;&#039;, dessen Endpunkte über in OBS gepflegte Pascal-Skripte realisiert werden. Jeder Endpunkt bekommt eine eigene Adresse, eine eigene Logik und eine eigene Berechtigungsstruktur. Rückgaben erfolgen ausschliesslich im JSON-Format. Damit lassen sich beliebige Lese- und Schreibvorgänge auf der OBS-Datenbank realisieren, ohne dass externe Systeme direkten Datenbank-Zugriff erhalten - das ganze OBS-Regelwerk (Rechte, Validierung, Geschäftslogik) bleibt aktiv.&lt;br /&gt;
&lt;br /&gt;
Im Ergebnis ist der REST-Server kein einzelnes Feature, sondern ein &#039;&#039;&#039;Werkzeugkasten&#039;&#039;&#039;: Was an Daten oder Funktionen in OBS verfügbar ist, kann über den REST-Server auch nach außen angeboten - oder von außen entgegengenommen - werden. Die Bandbreite reicht vom einfachen Lese-Endpunkt für ein Webseiten-Widget bis hin zu kompletten B2B-Integrationen mit Mandanten- und Rollen-Trennung.&lt;br /&gt;
&lt;br /&gt;
Aufruf des Moduls in OBS:&lt;br /&gt;
&#039;&#039;&#039;Stammdaten&#039;&#039;&#039; -&amp;gt; &#039;&#039;&#039;Z Weitere Stammdaten&#039;&#039;&#039; -&amp;gt; &#039;&#039;&#039;REST-Server&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Anwendungsbereiche==&lt;br /&gt;
&lt;br /&gt;
Die folgenden Einsatzfälle sind typische Beispiele, an denen sich der praktische Nutzen ablesen lässt. Es ist keine abschliessende Liste - alles, was sich in OBS-Skripten abbilden lässt, kann auch über einen Endpunkt bereitgestellt werden.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Dies sind Beispiele. Für die konkrete Entwicklung können, je nach Komplexität, weitere Kosten anfallen.}}&lt;br /&gt;
&lt;br /&gt;
===Webseiten und Kundenportale===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Live-Daten auf der eigenen Webseite:&#039;&#039;&#039; Lagerbestände, Verfügbarkeiten, Preise, Auftragsstatus aus OBS direkt anzeigen, ohne tägliche Datei-Exports.&lt;br /&gt;
* &#039;&#039;&#039;Kundenportale:&#039;&#039;&#039; Kunden sehen ihre offenen Posten, Auftragshistorie, Lieferscheine. Die Seite holt sich die Daten zur Anzeigezeit direkt aus OBS, jeder Kunde sieht nur seine eigenen Daten.&lt;br /&gt;
* &#039;&#039;&#039;Trackingseiten:&#039;&#039;&#039; Status einer Bestellung, eines Auftrags oder einer Reparatur für Endkunden, abrufbar über eine Sendungsnummer.&lt;br /&gt;
&lt;br /&gt;
===Mobile Anwendungen und Außendienst===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Mobile Zeiterfassung:&#039;&#039;&#039; Mitarbeiter buchen Anfang, Pause und Ende über Smartphone oder Tablet, die Daten landen direkt in OBS - ohne Zettelwirtschaft.&lt;br /&gt;
* &#039;&#039;&#039;QR-/Barcode-Scanner und Lagergeräte:&#039;&#039;&#039; Wareneingang, Inventur, Kommissionierung direkt in OBS abbilden, ohne zwischengeschaltete Software.&lt;br /&gt;
* &#039;&#039;&#039;Außendienst-Apps:&#039;&#039;&#039; Techniker sehen ihre Aufträge, dokumentieren Einsätze, erfassen Material - synchronisiert mit OBS.&lt;br /&gt;
&lt;br /&gt;
===Anbindung von Drittsystemen===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Webshops:&#039;&#039;&#039; Bestellungen werden direkt vom Shop in OBS gemeldet, Lagerbestände und Preise vom Shop bei OBS abgefragt.&lt;br /&gt;
* &#039;&#039;&#039;Buchhaltungs- und ERP-Systeme:&#039;&#039;&#039; Bidirektionaler Austausch von Belegen, Stammdaten und Buchungen.&lt;br /&gt;
* &#039;&#039;&#039;CRM- und Marketing-Tools:&#039;&#039;&#039; Personendaten, Aktivitäten oder Mailing-Empfänger werden synchron gehalten.&lt;br /&gt;
* &#039;&#039;&#039;Logistikdienstleister:&#039;&#039;&#039; Versandaufträge werden übergeben, Tracking-Informationen zurückgeliefert.&lt;br /&gt;
&lt;br /&gt;
===Automatisierte Rückmeldungen (WebHooks)===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Zahlungs-Callbacks:&#039;&#039;&#039; Bezahldienste (z.B. PayPal, Klarna, Stripe) melden den Zahlungseingang an einen WebHook-Endpunkt - OBS bucht automatisch.&lt;br /&gt;
* &#039;&#039;&#039;Versanddienste:&#039;&#039;&#039; Statusmeldungen (verschickt, zugestellt, retour) werden direkt im passenden Vorgang dokumentiert.&lt;br /&gt;
* &#039;&#039;&#039;Cloud-Workflows:&#039;&#039;&#039; Externe Automatisierungs-Plattformen (Make, Zapier, n8n, ...) stoßen OBS-Vorgänge an oder werden von OBS aus angesprochen.&lt;br /&gt;
&lt;br /&gt;
===Geschäftspartner-Kommunikation (B2B)===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lieferanten-Schnittstelle:&#039;&#039;&#039; Bestellungen werden automatisch übergeben, Auftragsbestätigungen und Lieferavise zurückgespielt.&lt;br /&gt;
* &#039;&#039;&#039;Kunden-Schnittstelle:&#039;&#039;&#039; Grosskunden bestellen direkt aus ihrem ERP heraus, Stammdaten und Konditionen werden zentral gepflegt.&lt;br /&gt;
* &#039;&#039;&#039;Sichere Maschine-zu-Maschine-Kommunikation:&#039;&#039;&#039; Mit Mutual-TLS (mTLS) wird sichergestellt, dass nur die freigeschalteten Partnersysteme - und niemand sonst - mit OBS sprechen können.&lt;br /&gt;
&lt;br /&gt;
===Interne Microservices und Automatisierungen===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Eigene kleine Hilfs-Tools:&#039;&#039;&#039; Zeitschaltautomatik, Reporting-Generator, Sammelaktionen, die aus mehreren Quellen Daten in OBS verarbeiten.&lt;br /&gt;
* &#039;&#039;&#039;Verbindung mehrerer Standorte:&#039;&#039;&#039; Verteilte Systeme tauschen Daten über definierte Endpunkte aus, ohne offene Datenbankverbindungen.&lt;br /&gt;
* &#039;&#039;&#039;Datenbereitstellung für Dashboards und Auswertungen:&#039;&#039;&#039; BI-Tools (Power BI, Grafana, ...) lesen aufbereitete Kennzahlen direkt aus OBS.&lt;br /&gt;
&lt;br /&gt;
===Was bedeutet das wirtschaftlich?===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Manuelle Arbeit fällt weg.&#039;&#039;&#039; Daten werden nicht mehr aus OBS exportiert, per Mail geschickt und woanders eingelesen - sie fliessen direkt.&lt;br /&gt;
* &#039;&#039;&#039;Fehler werden weniger.&#039;&#039;&#039; Jede manuelle Stelle ist eine mögliche Fehlerquelle; jede Automatisierung eine Stelle, an der nichts mehr schiefgehen kann.&lt;br /&gt;
* &#039;&#039;&#039;Neue Geschäftsmodelle werden möglich.&#039;&#039;&#039; Kunden- und Lieferantenportale, Self-Service-Funktionen, App-Anbindungen lassen sich aufbauen, ohne ein zweites System pflegen zu müssen.&lt;br /&gt;
* &#039;&#039;&#039;Bestehende Software bleibt verbunden.&#039;&#039;&#039; Statt eine vorhandene Anwendung ablösen zu müssen, kann sie über den REST-Server an OBS angedockt werden - in beiden Richtungen.&lt;br /&gt;
* &#039;&#039;&#039;Skaliert mit:&#039;&#039;&#039; Was klein anfängt (ein einzelner Endpunkt für eine Webseite) kann zu einer kompletten Integrations-Plattform ausgebaut werden, ohne dass die Grundlage gewechselt werden muss.&lt;br /&gt;
&lt;br /&gt;
==Architektur im Überblick==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server besteht aus mehreren aufeinander aufbauenden Bausteinen, die alle in OBS gepflegt werden:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Baustein     !! Tabelle              !! Zweck&lt;br /&gt;
|-&lt;br /&gt;
| Server       || RESTSRV_SERVER       || TLS-Profil + eigene HTTP-Server-Instanz, kann mehrfach existieren&lt;br /&gt;
|-&lt;br /&gt;
| Bindung      || RESTSRV_BINDINGS     || IP-Adresse + Port pro Server&lt;br /&gt;
|-&lt;br /&gt;
| Zugang       || RESTSRV_ACCOUNT      || API-Key, optionale Host- und CORS-Beschränkung, optionale JWT-Konfiguration&lt;br /&gt;
|-&lt;br /&gt;
| Endpunkt     || RESTSRV_ENDPOINTS    || Skript, das die Anfrage bearbeitet, einem Server fest zugeordnet&lt;br /&gt;
|-&lt;br /&gt;
| Berechtigung || RESTSRV_ACCESS       || Verbindet Zugang und Endpunkt&lt;br /&gt;
|-&lt;br /&gt;
| Idempotenz   || RESTSRV_IDEMPOTENCY  || Speicher für Idempotency-Keys und die dazu gelieferte Antwort&lt;br /&gt;
|-&lt;br /&gt;
| Sitzung      || RESTSRV_TOKEN        || Ausgestellte Token-Paare je Sitzung; Grundlage für Rotation und Widerruf&lt;br /&gt;
|-&lt;br /&gt;
| Protokoll    || RESTSRV_PROTO        || Laufende Ereignisse, Fehler, Audit-Einträge&lt;br /&gt;
|-&lt;br /&gt;
| Statistik    || RESTSRV_STATS        || Aufrufzähler, Antwortzeiten und HTTP-Status pro Endpunkt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Eine Anfrage durchläuft folgende Stationen:&lt;br /&gt;
&lt;br /&gt;
# Rate-Limit-Prüfung (pro API-Key bzw. IP und global, Schwellen je Server-Profil)&lt;br /&gt;
# Andrangsgrenze des Server-Profils (&#039;&#039;Max. parallel&#039;&#039;) - bei Überschreitung &#039;&#039;&#039;503&#039;&#039;&#039; mit &#039;&#039;Retry-After&#039;&#039;&lt;br /&gt;
# Authentifizierung über den API-Key (Header &#039;&#039;apikey&#039;&#039;)&lt;br /&gt;
# CORS-Prüfung (sofern &#039;&#039;Origin&#039;&#039;-Header gesetzt)&lt;br /&gt;
# Optional: JWT-Prüfung - Signatur, Ablauf und Sitzung (sofern für den Zugang aktiviert)&lt;br /&gt;
# Routing: Auflösung des Pfads über die Pfad-Templates der Endpunkte&lt;br /&gt;
# Prüfung der Berechtigung (Zugang -&amp;gt; Endpunkt)&lt;br /&gt;
# Entpacken des Anfragekörpers, sofern er mit &#039;&#039;Content-Encoding: gzip&#039;&#039; gesendet wurde&lt;br /&gt;
# Idempotenz-Prüfung bei POST/PUT/PATCH/DELETE - der Header &#039;&#039;Idempotency-Key&#039;&#039; ist dort &#039;&#039;&#039;Pflicht&#039;&#039;&#039;&lt;br /&gt;
# Ausführung des Endpunkt-Skripts (oder WebHook-Antwort)&lt;br /&gt;
# Festschreiben des Ergebnisses im Idempotenz-Speicher (sofern in Schritt 9 reserviert)&lt;br /&gt;
# JSON-Antwort und Statistik-Eintrag&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Die Reihenfolge ist kein Zufall.&#039;&#039;&#039; Das Entpacken liegt &#039;&#039;&#039;hinter&#039;&#039;&#039;&lt;br /&gt;
der Authentifizierung, damit ein Unbefugter dem Server keine Rechenzeit für eine&lt;br /&gt;
Dekompressionsbombe abverlangen kann - und &#039;&#039;&#039;vor&#039;&#039;&#039; der Idempotenz-Reservierung,&lt;br /&gt;
damit eine abgewiesene Anfrage keinen &#039;&#039;Idempotency-Key&#039;&#039; verbrennt.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Der Normalfall erzeugt keine Protokollzeile.&#039;&#039;&#039; Bis 08/2026 schrieb jede&lt;br /&gt;
Anfrage eine Zeile nach RESTSRV_PROTO; sie ist entfallen, weil RESTSRV_STATS&lt;br /&gt;
zu derselben Anfrage IP, Zugang, Endpunkt, Methode, Pfad, Status, Laufzeit und&lt;br /&gt;
dieselbe Korrelations-ID trägt - und länger aufbewahrt wird. Das Protokoll trägt&lt;br /&gt;
seitdem nur noch Auffälligkeiten.}}&lt;br /&gt;
&lt;br /&gt;
==Adressierung==&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt wird über ein &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; angesprochen, das den Pfad nach dem Host beschreibt und Platzhalter enthalten kann:&lt;br /&gt;
&lt;br /&gt;
 http://[Hostadresse][:Port][Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Beispiele:&lt;br /&gt;
&lt;br /&gt;
 https://api.meinserver.de/kalender/v1&lt;br /&gt;
 https://api.meinserver.de/orders/{uid}&lt;br /&gt;
 https://api.meinserver.de/orders/{uid}/modules/{code}&lt;br /&gt;
&lt;br /&gt;
Statische Segmente müssen exakt passen; Platzhalter in geschweiften Klammern (z.B. {uid}) matchen einen beliebigen Wert und stehen im Skript als Pfad-Parameter zur Verfügung. Eine Versionskennung wird als statisches Segment ins Template aufgenommen (z.B. /kalender/v1), um eine Funktionalität zu verändern, ohne bestehende Konsumenten zu brechen. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].&lt;br /&gt;
&lt;br /&gt;
==Unterstützte HTTP-Methoden==&lt;br /&gt;
&lt;br /&gt;
Der Server akzeptiert die Methoden &#039;&#039;&#039;GET&#039;&#039;&#039;, &#039;&#039;&#039;POST&#039;&#039;&#039;, &#039;&#039;&#039;PUT&#039;&#039;&#039;, &#039;&#039;&#039;DELETE&#039;&#039;&#039; und &#039;&#039;&#039;PATCH&#039;&#039;&#039; sowie &#039;&#039;&#039;OPTIONS&#039;&#039;&#039; (CORS-Preflight, ohne Skript-Aufruf). Jede Methode wird im Endpunkt-Skript als gleichnamige Prozedur implementiert - &#039;&#039;&#039;mit einer Ausnahme&#039;&#039;&#039;: &#039;&#039;Delete&#039;&#039; ist in der Skriptsprache ein reservierter Name und lässt sich nicht als Prozedur deklarieren. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
Fehlt die zur Methode passende Prozedur, antwortet der Server mit &#039;&#039;&#039;405&#039;&#039;&#039; und&lt;br /&gt;
nennt im Header &#039;&#039;Allow&#039;&#039; die Verben, die dieses Skript tatsächlich anbietet.&lt;br /&gt;
&lt;br /&gt;
==Kompression==&lt;br /&gt;
&lt;br /&gt;
Der Server packt Antworten mit &#039;&#039;&#039;gzip&#039;&#039;&#039;, wenn der Client sie annimmt. Ausgehandelt wird über den Anfrage-Header &amp;lt;code&amp;gt;Accept-Encoding&amp;lt;/code&amp;gt;; die Antwort trägt dann &amp;lt;code&amp;gt;Content-Encoding: gzip&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;Vary: Accept-Encoding&amp;lt;/code&amp;gt; - bei einer CORS-Anfrage steht dort &amp;lt;code&amp;gt;Vary: Origin, Accept-Encoding&amp;lt;/code&amp;gt;, also &#039;&#039;&#039;eine&#039;&#039;&#039; Zeile mit beiden Bedingungen. Browser, &amp;lt;code&amp;gt;curl --compressed&amp;lt;/code&amp;gt; und die gängigen HTTP-Bibliotheken entpacken selbstständig - für den Konsumenten ändert sich nichts ausser der übertragenen Datenmenge.&lt;br /&gt;
&lt;br /&gt;
Gepackt wird nur, wo es etwas bringt:&lt;br /&gt;
&lt;br /&gt;
* Nur JSON (auch &#039;&#039;problem+json&#039;&#039; und &#039;&#039;ndjson&#039;&#039;), XML, JavaScript, SVG und alles unter &#039;&#039;text/&#039;&#039; - darunter Text, CSV und HTML. Bereits komprimierte Typen - PDF, Bild, ZIP, Video - werden durch gzip &#039;&#039;&#039;grösser&#039;&#039;&#039;, nicht kleiner.&lt;br /&gt;
* Erst ab &#039;&#039;&#039;1024 Byte&#039;&#039;&#039;. Darunter kostet der gzip-Rahmen mehr, als der Inhalt einspart.&lt;br /&gt;
* &#039;&#039;&#039;Nicht&#039;&#039;&#039; bei Dateiantworten, nicht bei Teilauslieferungen (206) und nicht bei Antworten ohne Körper (204, 304).&lt;br /&gt;
* Nicht, wenn das Endpunkt-Skript den Header &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; selbst gesetzt hat - dann gehört die Antwort dem Skript.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;deflate&#039;&#039;&#039; wird in &#039;&#039;&#039;keiner&#039;&#039;&#039; Richtung unterstützt. Der Wert ist historisch uneindeutig - die einen senden RFC-1950-gerahmt, die anderen roh -, und wer ihn nennt, beherrscht ohnehin gzip. Eine Anfrage mit &amp;lt;code&amp;gt;Content-Encoding: deflate&amp;lt;/code&amp;gt; wird mit 415 abgewiesen.&lt;br /&gt;
&lt;br /&gt;
Abschalten lässt sich das Packen je Server-Profil über den Haken &#039;&#039;Keine Kompression&#039;&#039; (siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]). Der Haken betrifft ausschliesslich die Antwortrichtung.&lt;br /&gt;
&lt;br /&gt;
===Komprimierte Anfragen===&lt;br /&gt;
&lt;br /&gt;
In der Gegenrichtung nimmt der Server &#039;&#039;&#039;gzip&#039;&#039;&#039; entgegen: Trägt eine Anfrage den Header &amp;lt;code&amp;gt;Content-Encoding: gzip&amp;lt;/code&amp;gt;, wird der Körper entpackt, bevor ihn jemand liest. Das Endpunkt-Skript merkt davon nichts.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Anfrage                                              !! Antwort&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; fehlt oder &amp;lt;code&amp;gt;identity&amp;lt;/code&amp;gt; || normale Verarbeitung; ab 10 MB Körpergrösse &#039;&#039;&#039;413&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;gzip&amp;lt;/code&amp;gt;                                    || Körper wird entpackt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;deflate&amp;lt;/code&amp;gt;, anderer Wert (z.B. &amp;lt;code&amp;gt;br&amp;lt;/code&amp;gt;) oder mehrere Werte || &#039;&#039;&#039;415&#039;&#039;&#039; &amp;lt;code&amp;gt;UNSUPPORTED_MEDIA_TYPE&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Gepackter Körper bei einem Datei-Upload              || &#039;&#039;&#039;415&#039;&#039;&#039; - siehe [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]]&lt;br /&gt;
|-&lt;br /&gt;
| Entpackter Körper über &#039;&#039;&#039;10 MB&#039;&#039;&#039;                    || &#039;&#039;&#039;413&#039;&#039;&#039; &amp;lt;code&amp;gt;PAYLOAD_TOO_LARGE&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Entpackter Körper über dem &#039;&#039;&#039;400fachen&#039;&#039;&#039; der gesendeten Grösse || &#039;&#039;&#039;413&#039;&#039;&#039; &amp;lt;code&amp;gt;PAYLOAD_TOO_LARGE&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Für einen gepackten Anfragekörper gelten &#039;&#039;&#039;zwei&#039;&#039;&#039; Grenzen. Beide zählen den &#039;&#039;&#039;entpackten&#039;&#039;&#039; Inhalt, und beide greifen bereits &#039;&#039;&#039;während&#039;&#039;&#039; des Entpackens - ein Körper, der sie reisst, belegt den Speicher gar nicht erst:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;10 MB&#039;&#039;&#039; absolut. Dieselbe Grenze, die auch für einen ungepackten Körper gilt.&lt;br /&gt;
* &#039;&#039;&#039;das 400fache&#039;&#039;&#039; der gesendeten Grösse, &#039;&#039;&#039;mindestens aber 64 KB&#039;&#039;&#039;. Damit wird eine &#039;&#039;Dekompressionsbombe&#039;&#039; - wenige Kilobyte, die sich auf Hunderte Megabyte aufblähen - schon nach wenigen Prozent abgebrochen. Der Boden von 64 KB sorgt dafür, dass die Verhältnisrechnung winzige Körper gar nicht erst trifft: ein 100-Byte-Körper darf sich auf 64 KB entpacken, nicht nur auf 40 KB.&lt;br /&gt;
&lt;br /&gt;
Die zweite Grenze trifft normale Nutzlasten nicht. Gemessen erreichen echte JSON-Körper Verhältnisse von 24:1 bis 40:1, und selbst eine Liste aus zehntausenden identischen Zeilen kommt nur auf rund 340:1 - mehr gibt das Verfahren für wiederholte Datensätze nicht her. Über 400:1 kommt nur, wer sehr lange Läufe desselben Zeichens sendet.&lt;br /&gt;
&lt;br /&gt;
Für Datei-Uploads gilt keine der beiden Grenzen - dort zählt allein die je Endpunkt eingestellte Maximalgrösse.}}&lt;br /&gt;
&lt;br /&gt;
==Persistente Verbindungen (Keep-alive)==&lt;br /&gt;
&lt;br /&gt;
Der Server hält eine Verbindung nach der Antwort &#039;&#039;&#039;offen&#039;&#039;&#039;, statt sie zu&lt;br /&gt;
schliessen. Ein Client, der mehrere Anfragen hintereinander stellt - eine App,&lt;br /&gt;
die eine Liste seitenweise abholt -, spart damit je Folgeanfrage den Aufbau von&lt;br /&gt;
TCP-Verbindung und TLS-Sitzung.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Grenze !! Wert !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| Leerlauf || &#039;&#039;&#039;10 Sekunden&#039;&#039;&#039; || Passiert auf der Verbindung so lange nichts, wird sie geschlossen&lt;br /&gt;
|-&lt;br /&gt;
| Anfragen je Verbindung || &#039;&#039;&#039;100&#039;&#039;&#039; || Danach schliesst der Server geordnet, der Client baut neu auf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beides ist für den Konsumenten unkritisch: Eine geschlossene Verbindung ist kein&lt;br /&gt;
Fehler, jede HTTP-Bibliothek baut sie bei der nächsten Anfrage selbst wieder auf.&lt;br /&gt;
Wer viele Anfragen hintereinander stellt, gewinnt aber deutlich, wenn er seinen&lt;br /&gt;
Client die Verbindung wiederverwenden lässt (&#039;&#039;Connection: keep-alive&#039;&#039; ist der&lt;br /&gt;
Standard von HTTP/1.1, die meisten Bibliotheken tun es von allein).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Die Leerlaufgrenze gilt für jeden Lesevorgang der Verbindung&#039;&#039;&#039;,&lt;br /&gt;
nicht nur für das Warten auf die nächste Anfrage. Ein Client, der seinen&lt;br /&gt;
Anfragekörper nur tropfenweise sendet und dazwischen länger als 10 Sekunden&lt;br /&gt;
pausiert, verliert die Verbindung mitten im Senden.}}&lt;br /&gt;
&lt;br /&gt;
Die Werte sind bewusst knapp gewählt: Der Server fährt einen eigenen Thread je&lt;br /&gt;
Verbindung, eine leerlaufende Verbindung kostet also nicht bloss einen&lt;br /&gt;
Dateizeiger. Sie sind keine Lastgrenze - dafür gibt es &#039;&#039;Max. Verbindungen&#039;&#039; und&lt;br /&gt;
&#039;&#039;Max. parallel&#039;&#039; am Server-Profil (siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]).&lt;br /&gt;
&lt;br /&gt;
==Datei-Uploads==&lt;br /&gt;
&lt;br /&gt;
Endpunkte, die dafür freigeschaltet sind (&amp;lt;code&amp;gt;re_upload&amp;lt;/code&amp;gt;), nehmen Dateien&lt;br /&gt;
per &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; oder als fortsetzbaren &#039;&#039;&#039;Resumable-Upload&#039;&#039;&#039;&lt;br /&gt;
(&amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;) entgegen. Die maximale Grösse wird pro Endpunkt&lt;br /&gt;
festgelegt (&amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;, Standard 25&amp;amp;nbsp;MB); Überschreitung&lt;br /&gt;
ergibt 413, ein Upload an einen nicht freigeschalteten Endpunkt 415. Der Server&lt;br /&gt;
legt die Datei temporär ab und übergibt dem Skript den Pfad; die weitere&lt;br /&gt;
Verarbeitung (z.B. DMS-Ablage) übernimmt das Skript. Details:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
==Doppelte Sendungen abfangen (Idempotenz)==&lt;br /&gt;
&lt;br /&gt;
Ein Client, der nach einem Timeout dieselbe Anfrage erneut schickt, darf keine&lt;br /&gt;
Zweitbuchung auslösen. Der Server erledigt das selbst: Jeder Aufruf mit&lt;br /&gt;
&#039;&#039;&#039;POST&#039;&#039;&#039;, &#039;&#039;&#039;PUT&#039;&#039;&#039;, &#039;&#039;&#039;PATCH&#039;&#039;&#039; oder &#039;&#039;&#039;DELETE&#039;&#039;&#039; muss den Header&lt;br /&gt;
&amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; tragen; der Server merkt sich Schlüssel und Ergebnis&lt;br /&gt;
in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;. Eine Wiederholung mit demselben Schlüssel und&lt;br /&gt;
demselben Inhalt bekommt die &#039;&#039;&#039;gespeicherte Antwort&#039;&#039;&#039; zurück - das&lt;br /&gt;
Endpunkt-Skript läuft gar nicht erst an.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Der Header ist Pflicht, nicht optional.&#039;&#039;&#039; Ein schreibender Aufruf&lt;br /&gt;
&#039;&#039;&#039;ohne&#039;&#039;&#039; &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; wird mit &#039;&#039;&#039;400&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt; abgewiesen, und das Endpunkt-Skript läuft&lt;br /&gt;
nicht an.&lt;br /&gt;
&lt;br /&gt;
Das war bis v1.21 anders: fehlte der Header, lief die Anfrage ungeschützt durch.&lt;br /&gt;
Der Schutz gegen eine Doppelbuchung stand damit in der Zusage des Clients und&lt;br /&gt;
nicht in einer Prüfung des Servers - ein Wiederholversuch nach Zeitüberschreitung&lt;br /&gt;
buchte ein zweites Mal. Konsumenten, die bisher ohne den Header schreiben, müssen&lt;br /&gt;
angepasst werden.}}&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Pflicht nur für schreibende Verben.&#039;&#039;&#039; &#039;&#039;GET&#039;&#039; braucht keinen Schlüssel und kennt die Mechanik nicht.&lt;br /&gt;
* &#039;&#039;&#039;Der JWT-Endpunkt ist ausgenommen.&#039;&#039;&#039; Anmelden, Erneuern und Abmelden laufen vor der Adressauflösung und brauchen keinen Schlüssel.&lt;br /&gt;
* &#039;&#039;&#039;Kein Schalter am Endpunkt.&#039;&#039;&#039; Die Mechanik greift für jeden Endpunkt automatisch.&lt;br /&gt;
* &#039;&#039;&#039;Das Skript muss nichts tun.&#039;&#039;&#039; Eine eigene Idempotenz-Logik im Skript ist nicht nötig.&lt;br /&gt;
* &#039;&#039;&#039;Pro Vorgang ein Schlüssel&#039;&#039;&#039;, über alle Wiederholversuche hinweg unverändert - nicht bei jedem Versuch neu erzeugt.&lt;br /&gt;
&lt;br /&gt;
Details und die Statuscodes: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]], Abschnitt Idempotenz.&lt;br /&gt;
&lt;br /&gt;
==Sitzungen und Abmelden==&lt;br /&gt;
&lt;br /&gt;
Ein JWT trägt seine Gültigkeit in sich: der Server prüft Signatur und Ablauf und&lt;br /&gt;
braucht dafür keine Datenbank. Genau deshalb liesse sich ein ausgegebener Token&lt;br /&gt;
von sich aus auch nicht mehr zurückziehen - ein verlorenes Gerät wäre bis zum&lt;br /&gt;
Ablauf des Tokens weiter nutzbar.&lt;br /&gt;
&lt;br /&gt;
Der Server führt deshalb zu jedem ausgestellten Token-Paar eine Zeile in&lt;br /&gt;
&#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; und prüft jeden Zugriff dagegen. Daraus ergeben sich drei&lt;br /&gt;
Eigenschaften:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Abmelden wirkt sofort.&#039;&#039;&#039; Ein &#039;&#039;DELETE&#039;&#039; auf den JWT-Endpunkt sperrt die Sitzung; alle Token dieser Sitzung sind damit unmittelbar ungültig, auch die noch nicht abgelaufenen. Anmelden, Erneuern und Abmelden laufen also über einen einzigen Endpunkt, und für das Abmelden ist kein Skript nötig.&lt;br /&gt;
* &#039;&#039;&#039;Ein Refresh-Token gilt genau einmal.&#039;&#039;&#039; Beim Erneuern wird es entwertet. Wird es später erneut vorgelegt, gilt das als Diebstahl oder Fehlfunktion: die betroffene Sitzung wird komplett gesperrt und der Vorfall protokolliert.&lt;br /&gt;
* &#039;&#039;&#039;Alle Geräte abmelden.&#039;&#039;&#039; Ein gewöhnlicher Endpunkt kann über den Aufruf &#039;&#039;oWriter.JwtRevoke(&#039;all&#039;)&#039;&#039; auch alle Sitzungen eines Benutzers sperren - der Support-Fall „Gerät verloren&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Mit der Einführung dieser Prüfung verlieren &#039;&#039;&#039;alle vorher&lt;br /&gt;
ausgestellten Token&#039;&#039;&#039; ihre Gültigkeit, weil sie zu keiner Sitzung gehören.&lt;br /&gt;
Angemeldete Konsumenten müssen sich nach dem Update einmalig neu anmelden.}}&lt;br /&gt;
&lt;br /&gt;
Der Speicher pflegt sich selbst: Zeilen werden gelöscht, sobald sowohl das&lt;br /&gt;
Access- als auch das Refresh-Token abgelaufen sind. Details und die Skript-Seite:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Fehlerformat==&lt;br /&gt;
&lt;br /&gt;
Jede Fehlerantwort des Servers hat dieselbe Form: ein einziges &#039;&#039;error&#039;&#039;-Objekt&lt;br /&gt;
mit maschinenlesbarem Code, Support-Kennung, Meldung und Korrelations-ID:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;error&amp;quot;: {&lt;br /&gt;
    &amp;quot;code&amp;quot;:    &amp;quot;AUTH_EXPIRED&amp;quot;,&lt;br /&gt;
    &amp;quot;uid&amp;quot;:     &amp;quot;Y00E2RTPDY&amp;quot;,&lt;br /&gt;
    &amp;quot;message&amp;quot;: &amp;quot;Ungültiges oder abgelaufenes Token&amp;quot;,&lt;br /&gt;
    &amp;quot;traceId&amp;quot;: &amp;quot;20260817T091233123-00001A&amp;quot;&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;code&#039;&#039;&#039; - sprechender Bezeichner (z.B. &#039;&#039;AUTH_EXPIRED&#039;&#039;, &#039;&#039;NOT_FOUND&#039;&#039;, &#039;&#039;RATE_LIMITED&#039;&#039;, &#039;&#039;INTERNAL_ERROR&#039;&#039;), an dem ein Client seine Reaktion festmachen kann, ohne Texte auszuwerten.&lt;br /&gt;
* &#039;&#039;&#039;uid&#039;&#039;&#039; - OBS-Kennung der Codestelle. Für die Support-Recherche gedacht, nicht zur Auswertung im Client.&lt;br /&gt;
* &#039;&#039;&#039;message&#039;&#039;&#039; - Text für den Client. Interne Fehlerdetails stehen nie darin, sondern ausschliesslich im Protokoll &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;traceId&#039;&#039;&#039; - Korrelations-ID. Steht zusätzlich im Header &#039;&#039;X-Trace-Id&#039;&#039; und in jeder zugehörigen Logzeile.&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt-Skript kann eigene Codes setzen; siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;Fehlercode&#039;&#039;, &#039;&#039;Nachricht&#039;&#039; und &#039;&#039;traceId&#039;&#039; stehen &#039;&#039;&#039;nur&#039;&#039;&#039; im&lt;br /&gt;
&#039;&#039;error&#039;&#039;-Objekt, nicht flach daneben - der Wert von&lt;br /&gt;
&#039;&#039;Fehlercode&#039;&#039; heisst jetzt &#039;&#039;error.uid&#039;&#039;, &#039;&#039;Nachricht&#039;&#039; entspricht&lt;br /&gt;
&#039;&#039;error.message&#039;&#039;. Clients, die die flachen Felder auswerten, müssen angepasst&lt;br /&gt;
werden.}}&lt;br /&gt;
&lt;br /&gt;
==Sicherheitsmerkmale==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server enthält eine Reihe von Schutzmechanismen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Rate-Limit:&#039;&#039;&#039; Standardmässig max. 10 fehlgeschlagene und 120 erfolgreiche Anfragen pro API-Key (Account) bzw. IP und 60 Sekunden, max. 600 Anfragen global pro 60 Sekunden. Beim Überschreiten wird HTTP 429 mit &#039;&#039;Retry-After&#039;&#039;-Header zurückgegeben. Die vier Schwellen lassen sich &#039;&#039;&#039;pro Server-Profil&#039;&#039;&#039; hinterlegen; die Zähler laufen ebenfalls getrennt je Profil, ein überlastetes Profil bremst das andere also nicht mit aus. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
* &#039;&#039;&#039;Body-Limit:&#039;&#039;&#039; max. 10 MB Request-Body (JSON), max. 1024 Zeichen pro Header- oder Query-Parameter. Datei-Uploads laufen über einen separaten Pfad und sind pro Endpunkt auf &amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt; MB begrenzt (Standard 25&amp;amp;nbsp;MB).&lt;br /&gt;
* &#039;&#039;&#039;Sensible Felder&#039;&#039;&#039; (&#039;&#039;password&#039;&#039;, &#039;&#039;token&#039;&#039;, &#039;&#039;secret&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, ...) werden in Protokoll-Ausgaben automatisch maskiert.&lt;br /&gt;
* &#039;&#039;&#039;Widerrufbare Token:&#039;&#039;&#039; Jedes ausgestellte Token-Paar gehört zu einer Sitzung in &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; und wird bei &#039;&#039;&#039;jedem&#039;&#039;&#039; Zugriff dagegen geprüft. Eine Abmeldung wirkt damit sofort und nicht erst mit dem Ablauf des Tokens; ein Refresh-Token gilt genau einmal. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
* &#039;&#039;&#039;Reserviertes Präfix:&#039;&#039;&#039; Parameter-Namen mit Präfix &#039;&#039;_OBS_&#039;&#039; können von aussen nicht gesetzt werden, sie sind für den Server reserviert (z.B. JWT-Claims).&lt;br /&gt;
* &#039;&#039;&#039;Geblockte Header:&#039;&#039;&#039; &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, &#039;&#039;proxy-authorization&#039;&#039;, &#039;&#039;x-forwarded-for&#039;&#039;, &#039;&#039;x-real-ip&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;api_key&#039;&#039; werden nicht an das Skript durchgereicht.&lt;br /&gt;
* &#039;&#039;&#039;TLS:&#039;&#039;&#039; Bei Profilen mit aktivem TLS fährt der Server ohne Zertifikat und Key nicht hoch. Plain-HTTP ist nur für ein dediziertes Debug-Profil möglich.&lt;br /&gt;
* &#039;&#039;&#039;HSTS:&#039;&#039;&#039; Antworten eines TLS-Profils tragen &#039;&#039;Strict-Transport-Security: max-age=31536000; includeSubDomains&#039;&#039;. Ein Browser spricht die Adresse danach ein Jahr lang nur noch über HTTPS an. Beim Debug-Profil &#039;&#039;Kein SSL&#039;&#039; wird der Header bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; gesetzt - der Browser würde ihn sonst speichern und die Erzwingung auch auf produktive Sitzungen anwenden.&lt;br /&gt;
* &#039;&#039;&#039;mTLS&#039;&#039;&#039; (Mutual TLS): erzwingt, dass jeder Client ein gültiges Client-Zertifikat vorlegt. Optional kann pro Zugang ein erwarteter Subject-DN hinterlegt werden.&lt;br /&gt;
* &#039;&#039;&#039;DNS-Cache:&#039;&#039;&#039; Host-zu-IP-Auflösungen für die Zugangs-Host-Prüfung werden 5 Minuten gecached.&lt;br /&gt;
* &#039;&#039;&#039;Automatische Zertifikatserneuerung:&#039;&#039;&#039; Ein TLS-Profil kann sein Zertifikat selbst bei einer Zertifizierungsstelle beantragen und vor Ablauf erneuern. Der Dienst prüft wöchentlich, ob der Server dafür von aussen erreichbar ist, und tauscht das Zertifikat anschliessend ohne Neustart des Dienstes - nur das betroffene Profil startet neu. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
&lt;br /&gt;
==Protokoll==&lt;br /&gt;
&lt;br /&gt;
Die Tabelle &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; enthält alle relevanten Ereignisse: erfolgreiche und abgelehnte Anfragen, TLS-/JWT-Fehler, Bindungs-Ereignisse, Skript-Fehler. Einträge sind nach Datum, Uhrzeit und Host (IP oder Account-Name) sortiert. Das Protokoll ist die erste Anlaufstelle bei Störungen. Jede Antwort trägt zudem eine Korrelations-ID (Response-Header &#039;&#039;X-Trace-Id&#039;&#039;), die in den Protokoll-Einträgen wiederzufinden ist.&lt;br /&gt;
&lt;br /&gt;
Läuft der REST-Server hinter einem Reverse-Proxy, steht als Host die IP-Adresse des Proxys. Schickt der Proxy den Header &#039;&#039;X-Forwarded-For&#039;&#039; mit, hängt der Server dessen Wert in Protokoll und Konsole als &amp;lt;code&amp;gt;[xff=…]&amp;lt;/code&amp;gt; an den Eintrag an. Der Wert ist eine Angabe des Absenders und wird nur protokolliert. Host-Beschränkung und Rate-Limit verwenden immer die IP-Adresse der Verbindung. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung#Betrieb hinter einem Reverse-Proxy|Betrieb hinter einem Reverse-Proxy]].&lt;br /&gt;
&lt;br /&gt;
==Statistik==&lt;br /&gt;
&lt;br /&gt;
Die Tabelle &#039;&#039;&#039;RESTSRV_STATS&#039;&#039;&#039; enthält eine Statistik aller bearbeiteten Anfragen&lt;br /&gt;
mit IP, Zugang, Endpunkt, Methode, angefragtem Pfad, HTTP-Status und Laufzeit in&lt;br /&gt;
Millisekunden. Dazu kommen zwei Felder, die den Sprung ins Protokoll erlauben:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;rs_trace&#039;&#039;&#039; - dieselbe Korrelations-ID, die auch im Header &#039;&#039;X-Trace-Id&#039;&#039; und in jeder zugehörigen Protokollzeile steht. Damit findet man zu einer auffälligen Statistikzeile alle Protokolleinträge derselben Anfrage.&lt;br /&gt;
* &#039;&#039;&#039;rs_server&#039;&#039;&#039; - das Server-Profil, über das die Anfrage hereinkam. Bei mehreren Profilen (Public-TLS neben Internal-mTLS) ist das die einzige Angabe, an der sich der Weg ablesen lässt.&lt;br /&gt;
&lt;br /&gt;
Weil der Normalfall keine Protokollzeile mehr schreibt, ist RESTSRV_STATS die&lt;br /&gt;
vollständige Sicht auf den Betrieb - RESTSRV_PROTO die auf die Auffälligkeiten.&lt;br /&gt;
&lt;br /&gt;
Aufruf der Statistik:&lt;br /&gt;
&lt;br /&gt;
* Aus der Endpunkt-Liste: F8 zeigt die Statistik des markierten Endpunkts.&lt;br /&gt;
* Aus der Zugänge-Liste: F8 zeigt die Statistik des markierten Zugangs.&lt;br /&gt;
&lt;br /&gt;
==Konsole==&lt;br /&gt;
&lt;br /&gt;
Wird der REST-Server nicht als Dienst, sondern interaktiv gestartet, erscheint eine&lt;br /&gt;
farbige Konsole mit Live-Log und einem &#039;&#039;&#039;fixierten Kopfbereich&#039;&#039;&#039;, der die gerade&lt;br /&gt;
laufenden Anfragen zeigt und sich fortlaufend aktualisiert.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;show &amp;lt;handle&amp;gt;&#039;&#039; || Details einer Anfrage: Request-Header, Protokollzeilen, Antwort. JSON wird farbig formatiert. &#039;&#039;&amp;lt;handle&amp;gt;&#039;&#039; ist die Kennung aus der Abschlusszeile der Anfrage (z.B. &#039;&#039;7F&#039;&#039;), &#039;&#039;&#039;keine&#039;&#039;&#039; laufende Nummer&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;1&#039;&#039; … &#039;&#039;9&#039;&#039; || Auswahl im Kopfbereich - kürzer als &#039;&#039;show&#039;&#039; für die gerade sichtbaren Anfragen. Mit &#039;&#039;mouse on&#039;&#039; auch per Klick&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;ps&#039;&#039; || Listet die gerade laufenden Anfragen auf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;level &amp;lt;stufe&amp;gt;&#039;&#039; || Anzeigestufe des Live-Logs: &#039;&#039;quiet&#039;&#039; , &#039;&#039;normal&#039;&#039; oder &#039;&#039;debug&#039;&#039;. Ohne Parameter wird die aktuelle Stufe genannt&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;filter &amp;lt;ausdruck&amp;gt;&#039;&#039; || Blendet aus, was nicht passt: &#039;&#039;path=&amp;lt;text&amp;gt;&#039;&#039;, &#039;&#039;account=&amp;lt;name&amp;gt;&#039;&#039;, &#039;&#039;status&amp;gt;=&amp;lt;code&amp;gt;&#039;&#039;. &#039;&#039;filter aus&#039;&#039; hebt die Einschränkung auf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;panel on&amp;amp;#124;off&#039;&#039; || Fixierten Kopfbereich ein-/ausschalten. &#039;&#039;&#039;Ausgeschaltet&#039;&#039;&#039; lässt sich der Rückblick-Puffer des Konsolenfensters wieder scrollen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;mouse on&amp;amp;#124;off&#039;&#039; || Klick-Auswahl im Kopfbereich. &#039;&#039;&#039;Eingeschaltet&#039;&#039;&#039; ist das Markieren von Text mit der Maus nicht mehr möglich&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;help&#039;&#039; / &#039;&#039;?&#039;&#039; || Zeigt diese Liste in der Konsole&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;exit&#039;&#039; / &#039;&#039;quit&#039;&#039; || Beendet den Server geordnet&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Anzeigefilter wirkt &#039;&#039;&#039;nur auf die Anzeige&#039;&#039;&#039;. Was die Konsole&lt;br /&gt;
ausblendet, steht vollständig in RESTSRV_PROTO - es geht nichts verloren.}}&lt;br /&gt;
&lt;br /&gt;
Im Service-Modus (Windows-Dienst) ist die Konsole nicht sichtbar. Alle Einträge landen dort ausschliesslich in der Tabelle RESTSRV_PROTO.&lt;br /&gt;
&lt;br /&gt;
==Weiterführende Seiten==&lt;br /&gt;
&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]] - Schritt-für-Schritt-Anleitung&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]] - TLS, mTLS und Bindungen&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]] - Portfreigabe, Reverse-Proxy oder VPN im Vergleich&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]] - automatische Zertifikate über http-01 oder dns-01&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]] - API-Keys, JWT, CORS, Host-Beschränkung&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]] - Verwaltung, Berechtigungen, WebHooks&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]] - Aufbau der Endpunkt-Skripte&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]] - Übertragungsprotokoll für Dateien (Multipart, resumable)&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]] - Dateien ausliefern, Range/Resume, öffentliche Artefakte&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel1|Beispiel 1: Daten-Abruf mit JWT]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel2|Beispiel 2: Zeiterfassung als komplette Webanwendung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel3|Beispiel 3: Datensatz anlegen mit JSON-Body (CRUD)]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4: Pfad-Parameter im Routing]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel5|Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6: Datei-Upload und Ablage im DMS]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=67350</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Einrichtung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=67350"/>
		<updated>2026-11-05T12:39:52Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Navigationsvorlage mit Vorlage:-Praefix [Volltext ersetzt] [Styleguide-Abweichung: Seitenkopf der Altseite]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Einrichtung eines REST-Endpunktes=&lt;br /&gt;
&lt;br /&gt;
Die folgende Reihenfolge stellt sicher, dass ein lauffähiger Endpunkt mit möglichst wenigen Korrektur-Schleifen entsteht.&lt;br /&gt;
&lt;br /&gt;
==Vorbereitung==&lt;br /&gt;
&lt;br /&gt;
# OBS-Support kontaktieren und das REST-Server-Modul aktivieren lassen.&lt;br /&gt;
# Prüfen, ob auf dem Zielrechner (OBS-Server oder dedizierter REST-Server) Port und IP-Adresse frei sind.&lt;br /&gt;
# Bei TLS-Profilen: TLS-Zertifikat (PEM, mit Kette), privaten Schlüssel (PEM) und - falls mTLS - das CA-Root-Zertifikat bereithalten. Alternativ nutzen Sie die [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|automatische Zertifikatserneuerung]]. Wie der Server von aussen erreichbar wird und welche Variante sich eignet, beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 1: Server-Profil anlegen==&lt;br /&gt;
&lt;br /&gt;
In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server&#039;&#039;&#039; den Punkt &#039;&#039;&#039;Server&#039;&#039;&#039; öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; ein neues Profil anlegen. Pflichtfelder:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Nr&#039;&#039;&#039; - eindeutige Nummer (1-99), wird beim Anlegen automatisch vorgeschlagen.&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - sprechender Name (z.B. &#039;&#039;Public-API&#039;&#039;, &#039;&#039;Internal-mTLS&#039;&#039;, &#039;&#039;Debug-Local&#039;&#039;).&lt;br /&gt;
* Auswahl des Modus:&lt;br /&gt;
** &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039; (Default): Zertifikat und Schlüssel hinterlegen - oder auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; die automatische Erneuerung aktivieren.&lt;br /&gt;
** &#039;&#039;&#039;mTLS-Modus aktiv&#039;&#039;&#039; setzen: zusätzlich das CA-Root-Zertifikat hinterlegen, gegen das die Client-Zertifikate geprüft werden. Jeder Client muss dann ein gültiges Zertifikat vorlegen.&lt;br /&gt;
** &#039;&#039;&#039;kein SSL&#039;&#039;&#039; setzen: ausschliesslich für lokale Tests, der Server akzeptiert dann nur Plain-HTTP.&lt;br /&gt;
&lt;br /&gt;
Optional lassen sich am Profil eigene &#039;&#039;&#039;Rate-Limit-Schwellen&#039;&#039;&#039; hinterlegen. Ohne&lt;br /&gt;
Angabe gelten die Standardwerte (60-Sekunden-Fenster, 120 erfolgreiche und 10&lt;br /&gt;
fehlgeschlagene Anfragen je Schlüssel, 600 Anfragen je Profil). Für Anwendungen&lt;br /&gt;
ohne eigenen API-Key - typisch mobile Apps - sollten die Werte bewusst gesetzt&lt;br /&gt;
werden, weil dann alle Benutzer hinter einer IP auf denselben Zähler laufen.&lt;br /&gt;
&lt;br /&gt;
Details siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 2: Bindung anlegen==&lt;br /&gt;
&lt;br /&gt;
Im Server-Profil mit &#039;&#039;&#039;F6&#039;&#039;&#039; die Bindungs-Liste öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; eine neue Bindung anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - IP-Adresse, auf der gelauscht werden soll (z.B. &#039;&#039;0.0.0.0&#039;&#039; für alle Adressen, &#039;&#039;127.0.0.1&#039;&#039; für lokal).&lt;br /&gt;
* &#039;&#039;&#039;Port&#039;&#039;&#039; - Port-Nummer (typisch 443 für HTTPS, 8099 für Debug, ggf. abweichend). {{Achtung|Port 8080 wird für RustDesk verwendet und darf daher nicht verwendet werden}}&lt;br /&gt;
* &#039;&#039;&#039;Standard&#039;&#039;&#039; setzen, wenn diese Bindung die Standard-Bindung des Servers ist (pro Server-Profil nur eine).&lt;br /&gt;
&lt;br /&gt;
==Schritt 3: REST-Dienst starten / neustarten==&lt;br /&gt;
&lt;br /&gt;
Damit Änderungen an Server-Profilen oder Bindungen wirksam werden, muss der OBS REST-Server-Dienst (oder die Konsole) gestartet bzw. neugestartet werden. Das Profil wird beim Start aus der Datenbank gelesen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 4: Zugang anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Zugänge&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Zugang anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - erscheint im Protokoll und in der Statistik.&lt;br /&gt;
* &#039;&#039;&#039;API-Key&#039;&#039;&#039; - eindeutig, mit dem Schlüsselsymbol kann ein zufälliger Key generiert werden.&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - optional die IP oder der Hostname des Konsumenten (sollte, wenn möglich, immer gesetzt werden).&lt;br /&gt;
* Optional: &#039;&#039;&#039;CORS-Origins&#039;&#039;&#039; und &#039;&#039;&#039;JWT-Konfiguration&#039;&#039;&#039;, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 5: Endpunkt anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Endpunkte&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Endpunkt anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Endpunkt&#039;&#039;&#039; - Ressourcen-/Anzeigename (z.B. &#039;&#039;orders&#039;&#039;, &#039;&#039;kalender&#039;&#039;); dient nur der Übersicht, nicht dem Routing.&lt;br /&gt;
* &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; - der maßgebliche Routing-Pfad, ggf. mit Platzhaltern (z.B. &#039;&#039;/orders&#039;&#039;, &#039;&#039;/orders/{uid}&#039;&#039;, &#039;&#039;/orders/{uid}/modules/{code}&#039;&#039;).&lt;br /&gt;
* &#039;&#039;&#039;Server&#039;&#039;&#039; - die Server-Auswahl bestimmt, über welches Server-Profil der Endpunkt erreichbar ist.&lt;br /&gt;
* &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; - nur setzen, wenn der Endpunkt Dateien entgegennehmen soll; dann auch die &#039;&#039;&#039;Max. Upload-Grösse&#039;&#039;&#039; in MB festlegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6]]).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Eine Versionskennung wird als statisches Segment ins Pfad-Template aufgenommen (z.B. &#039;&#039;/orders/v1&#039;&#039;); ein eigenes Versionsfeld gibt es nicht. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].}}&lt;br /&gt;
&lt;br /&gt;
Anschliessend mit &#039;&#039;&#039;F7&#039;&#039;&#039; das Endpunkt-Skript pflegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]]).&lt;br /&gt;
&lt;br /&gt;
==Schritt 6: Zugang für den Endpunkt freischalten==&lt;br /&gt;
&lt;br /&gt;
Aus der Endpunkt-Liste den neuen Endpunkt markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die Berechtigungs-Liste. Mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; kann ein oder mehrere Zugänge ausgewählt und mit &#039;&#039;&#039;F2&#039;&#039;&#039; übernommen werden. Erst nach dieser Freischaltung darf ein Zugang den Endpunkt aufrufen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 7: Funktionsprüfung==&lt;br /&gt;
&lt;br /&gt;
Aufruf mit einem HTTP-Client (curl, Postman, Browser-Plug-in):&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; https://api.meinserver.de/[Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Bei korrektem Setup erscheint die JSON-Antwort des Skripts. Fehlerfälle werden in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; protokolliert.&lt;br /&gt;
&lt;br /&gt;
==Schritt 8: Statistik und Protokoll prüfen==&lt;br /&gt;
&lt;br /&gt;
* In der Endpunkt-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Endpunkts.&lt;br /&gt;
* In der Zugänge-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Zugangs.&lt;br /&gt;
* In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Protokoll&#039;&#039;&#039; die Ereignisse prüfen.&lt;br /&gt;
&lt;br /&gt;
==Betrieb hinter einem Reverse-Proxy==&lt;br /&gt;
&lt;br /&gt;
Soll der Server des REST-Dienstes nicht direkt aus dem Internet erreichbar sein, nimmt ein Reverse-Proxy die Anfragen entgegen, zum Beispiel in einer DMZ oder als Funktion der Firewall, und reicht sie an den REST-Dienst weiter. Von aussen ist nur der Proxy erreichbar. Vor- und Nachteile gegenüber Portfreigabe und VPN beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]].&lt;br /&gt;
&lt;br /&gt;
Am Proxy und im Netz richten Sie ein:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Öffentlicher Name:&#039;&#039;&#039; zeigt auf die öffentliche IP-Adresse des Proxys.&lt;br /&gt;
* &#039;&#039;&#039;Zertifikat:&#039;&#039;&#039; Der Proxy braucht ein öffentlich gültiges Zertifikat für diesen Namen. Die automatische Zertifikatserneuerung des REST-Servers wird nicht benötigt.&lt;br /&gt;
* &#039;&#039;&#039;Weiterleitung:&#039;&#039;&#039; Der Proxy leitet per HTTPS an &amp;lt;code&amp;gt;https://&amp;lt;Server des REST-Dienstes&amp;gt;:&amp;lt;Port&amp;gt;&amp;lt;/code&amp;gt; weiter. Für diese Strecke genügt am REST-Server ein Zertifikat Ihrer internen Zertifizierungsstelle oder ein selbst signiertes, dem der Proxy vertraut. Auch dieses Zertifikat läuft ab und muss vor Ablauf getauscht werden.&lt;br /&gt;
* &#039;&#039;&#039;Header:&#039;&#039;&#039; Alle HTTP-Methoden und Header unverändert durchreichen, insbesondere &#039;&#039;Authorization&#039;&#039;, &#039;&#039;apikey&#039;&#039; und &#039;&#039;Idempotency-Key&#039;&#039;. Antworten nicht zwischenspeichern. Den Header &#039;&#039;X-Forwarded-For&#039;&#039; mit der IP-Adresse des Konsumenten setzen.&lt;br /&gt;
* &#039;&#039;&#039;Anfragegrösse:&#039;&#039;&#039; Die maximale Anfragegrösse am Proxy darf nicht unter der Upload-Grenze der Endpunkte liegen.&lt;br /&gt;
* &#039;&#039;&#039;Firewall:&#039;&#039;&#039; Den Port des REST-Servers intern nur für den Proxy freigeben.&lt;br /&gt;
&lt;br /&gt;
Hinter dem Proxy verhält sich der REST-Server so:&lt;br /&gt;
&lt;br /&gt;
* Protokoll und Konsole zeigen als Host die IP-Adresse des Proxys und hängen den Wert von &#039;&#039;X-Forwarded-For&#039;&#039; als &amp;lt;code&amp;gt;[xff=…]&amp;lt;/code&amp;gt; an.&lt;br /&gt;
* Die [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge#Host-Beschränkung|Host-Beschränkung]] eines Zugangs kann keine einzelnen Konsumenten unterscheiden.&lt;br /&gt;
* Beim [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile#Rate-Limit je Profil|Rate-Limit]] teilen sich alle Konsumenten ohne API-Key einen Zähler.&lt;br /&gt;
&lt;br /&gt;
==Wartung der Server-Tabellen==&lt;br /&gt;
&lt;br /&gt;
Die Tabellen des REST-Servers pflegen sich selbst; ein regelmässiger Eingriff ist&lt;br /&gt;
nicht vorgesehen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Tabelle !! Aufräumen&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_IDEMPOTENCY || Abgeschlossene Einträge werden nach 30 Tagen gelöscht. Offene Reservierungen werden ab 24 Stunden im Protokoll gemeldet und &#039;&#039;&#039;anschliessend ebenfalls gelöscht&#039;&#039;&#039; - sie sind trotzdem ein Support-Fall, weil die Fachwirkung unbekannt ist, siehe Stolpersteine&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_TOKEN || Zeilen werden gelöscht, sobald Access- &#039;&#039;&#039;und&#039;&#039;&#039; Refresh-Laufzeit vorbei sind. Läuft beim Serverstart und danach höchstens stündlich&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_PROTO / RESTSRV_STATS || Wachsen mit dem Betrieb. Bei hohem Aufkommen empfiehlt sich ein turnusmässiges Archivieren&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Häufige Stolpersteine==&lt;br /&gt;
&lt;br /&gt;
* Bindung angelegt, aber der Server wurde nicht neugestartet -&amp;gt; die Bindung ist noch nicht aktiv.&lt;br /&gt;
* Endpunkt einem falschen Server zugeordnet -&amp;gt; Aufruf liefert 404 &#039;&#039;Endpunkt nicht vorhanden oder inaktiv&#039;&#039;.&lt;br /&gt;
* Pfad-Template falsch geschrieben oder Segmentzahl passt nicht -&amp;gt; kein Template matcht, Aufruf liefert 404.&lt;br /&gt;
* Zugang für den Endpunkt nicht freigeschaltet -&amp;gt; Aufruf liefert 403 &#039;&#039;Keine Berechtigung für die Endpunkt-Nutzung&#039;&#039;.&lt;br /&gt;
* Falscher API-Key oder falscher Host beim Zugang -&amp;gt; Aufruf liefert 401 bzw. 403.&lt;br /&gt;
* mTLS aktiv, aber kein Client-Zertifikat installiert -&amp;gt; Verbindung wird während des TLS-Handshakes abgebrochen.&lt;br /&gt;
* Datei-Upload ohne gesetzten Haken &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; am Endpunkt -&amp;gt; Aufruf liefert 415, das Skript läuft nicht an.&lt;br /&gt;
* Skript geändert, aber der Aufruf verhält sich noch wie vorher -&amp;gt; Endpunkt-Cache abwarten (bis zu 60 Sekunden).&lt;br /&gt;
* Schreibaufruf (POST/PUT/PATCH/DELETE) liefert 400 &#039;&#039;IDEMPOTENCY_KEY_MISSING&#039;&#039; -&amp;gt; der Client sendet keinen &#039;&#039;Idempotency-Key&#039;&#039;. Der Header ist seit v1.21 &#039;&#039;&#039;Pflicht&#039;&#039;&#039;, nicht optional; das Endpunkt-Skript läuft in diesem Fall gar nicht an. Betrifft typischerweise Konsumenten, die vor dem Update gebaut wurden.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 422 &#039;&#039;IDEMPOTENCY_KEY_REUSED&#039;&#039; -&amp;gt; der Client sendet denselben &#039;&#039;Idempotency-Key&#039;&#039; mit geändertem Inhalt. Pro Vorgang genau ein Schlüssel, über alle Wiederholversuche hinweg unverändert.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 409 &#039;&#039;IDEMPOTENCY_UNRESOLVED&#039;&#039; -&amp;gt; ein früherer Versuch wurde hart abgebrochen (Dienst beendet). Ob gebucht wurde, ist offen: im Protokoll und in den Fachdaten prüfen. Der Aufräumlauf meldet den Eintrag nach 24 Stunden im Protokoll und löscht ihn dann; wer schneller weitermachen will, entfernt ihn in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;.&lt;br /&gt;
* Nach einem Update antworten &#039;&#039;&#039;alle&#039;&#039;&#039; bestehenden Token mit 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; -&amp;gt; erwartetes Verhalten. Token, die vor der Einführung der Sitzungsprüfung ausgestellt wurden, gehören zu keiner Sitzung; die Konsumenten müssen sich einmalig neu anmelden.&lt;br /&gt;
* Anmeldung liefert 503 statt eines Tokens -&amp;gt; die Sitzungszeile konnte nicht geschrieben werden. Ursache im Protokoll unter &#039;&#039;TokenStore: ...&#039;&#039; nachsehen; typisch ist eine fehlende Tabelle &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; nach einem Update ohne Datenbank-Abgleich.&lt;br /&gt;
* Abmelden liefert 503 -&amp;gt; die Sitzung liess sich nicht sperren. Der Client ist &#039;&#039;&#039;nicht&#039;&#039;&#039; abgemeldet und muss den Aufruf wiederholen; Details im Protokoll.&lt;br /&gt;
* Techniker wird beim Arbeiten unerwartet abgemeldet -&amp;gt; im Protokoll nach &#039;&#039;WIEDERVORLAGE&#039;&#039; suchen. Ein doppelt verwendetes Refresh-Token sperrt die Sitzung. Legt die App Token mehrfach parallel ab, gehört das clientseitig behoben.&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/%C3%96ffentliche_Freigabe&amp;diff=67082</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/%C3%96ffentliche_Freigabe&amp;diff=67082"/>
		<updated>2026-10-26T10:31:32Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Vergleichstabelle: selbst signiertes Zertifikat zwischen Proxy und REST-Server genannt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
{{Seitentyp|Übersicht}}&lt;br /&gt;
= Öffentliche Freigabe =&lt;br /&gt;
&lt;br /&gt;
Apps auf mobilen Geräten und externe Systeme erreichen den REST-Server nur, wenn er aus dem Internet erreichbar ist. Diese Seite stellt die drei Wege dafür gegenüber und richtet sich an die IT-Administration, die über die Freigabe entscheidet.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der REST-Dienst läuft auf dem OBS-Server oder auf einem [[#REST-Dienst auf einem eigenen Server|eigenen Server]]. Auf dem OBS-Server läuft häufig auch die Datenbank. Die Varianten unterscheiden sich vor allem darin, ob eine Verbindung aus dem Internet den Server des REST-Dienstes direkt erreicht. Ein Zertifikat ist in allen Varianten nötig, auch im VPN, weil die App nur per HTTPS verbindet.}}&lt;br /&gt;
&lt;br /&gt;
== Varianten im Vergleich ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Merkmal !! Direkte Portfreigabe !! Reverse-Proxy !! VPN&lt;br /&gt;
|-&lt;br /&gt;
| Aus dem Internet erreichbar || Server des REST-Dienstes: Port des REST-Servers, bei Prüfart http-01 zusätzlich Port 80 || nur der Proxy (Port 443) || nur das VPN-Gateway&lt;br /&gt;
|-&lt;br /&gt;
| Aufwand für die Nutzer der App || keiner || keiner || VPN-Client einrichten und vor der Nutzung einschalten&lt;br /&gt;
|-&lt;br /&gt;
| Aufwand für die IT || Portweiterleitung am Router || Proxy betreiben, Zertifikat am Proxy || VPN-Zugang für jedes Gerät&lt;br /&gt;
|-&lt;br /&gt;
| Filtern und Protokollieren vor dem REST-Dienst || – || ✓ || nur auf Netzebene&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikat || im REST-Server, Prüfart http-01 oder dns-01 || öffentlich am Proxy, dazu eines im REST-Server für die Strecke vom Proxy (selbst signiert genügt) || im REST-Server, Prüfart dns-01&lt;br /&gt;
|-&lt;br /&gt;
| REST-Server sieht die IP-Adresse des Geräts || ✓ || nur im Protokoll (über &#039;&#039;X-Forwarded-For&#039;&#039;) || ✓, sofern das VPN-Gateway die Adressen nicht umsetzt (NAT)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Schutz durch den REST-Server ==&lt;br /&gt;
Unabhängig von der Variante schützt sich der REST-Server selbst. Diese Mechanismen greifen in allen drei Varianten:&lt;br /&gt;
* &#039;&#039;&#039;Verschlüsselung:&#039;&#039;&#039; ausschliesslich TLS 1.2 mit Forward Secrecy und AEAD-Verfahren. Unverschlüsselte Anfragen an einen TLS-Port werden vor jeder Verarbeitung abgewiesen. Optional mTLS: jeder Client braucht ein gültiges Client-Zertifikat.&lt;br /&gt;
* &#039;&#039;&#039;Zugänge:&#039;&#039;&#039; Jede Anfrage braucht einen gültigen API-Key, wahlweise zusätzlich JWT. Jeder Zugang ist nur für die Endpunkte freigeschaltet, die er braucht, und lässt sich auf eine Absenderadresse beschränken. API-Keys lassen sich ohne Unterbrechung tauschen.&lt;br /&gt;
* &#039;&#039;&#039;Widerrufbare Sitzungen:&#039;&#039;&#039; Eine Abmeldung wirkt sofort. Ein Refresh-Token gilt genau einmal; wird ein verbrauchtes erneut vorgelegt, sperrt der Server die ganze Sitzung.&lt;br /&gt;
* &#039;&#039;&#039;Grenzen gegen Missbrauch:&#039;&#039;&#039; getrenntes Rate-Limit für erfolgreiche und fehlgeschlagene Anfragen, eine Obergrenze gleichzeitiger Anfragen und Verbindungen sowie Grössengrenzen für Anfragen und Uploads.&lt;br /&gt;
* &#039;&#039;&#039;Keine Informationen nach aussen:&#039;&#039;&#039; Fehlerantworten enthalten nie interne Details. Die stehen nur im Protokoll, mit einer Korrelations-ID je Anfrage. Kennwörter und Token werden dort maskiert.&lt;br /&gt;
* &#039;&#039;&#039;Kurze Öffnung für die Zertifikatsprüfung:&#039;&#039;&#039; Bei http-01 öffnet der Dienst Port 80 nur für die Dauer der Prüfung.&lt;br /&gt;
&lt;br /&gt;
Diese Mechanismen schützen den REST-Dienst. Reverse-Proxy und VPN fügen eine Schicht davor hinzu: Sie halten den Server des REST-Dienstes aus dem Internet heraus, und mit ihm häufig auch den OBS-Server und die Datenbank. Das wirkt auch gegen Schwachstellen ausserhalb des REST-Servers und gegen Überlastung.&lt;br /&gt;
Der Router leitet den Port des REST-Servers aus dem Internet auf den Server des REST-Dienstes weiter. Für die Zertifikatserneuerung mit der Prüfart http-01 wird zusätzlich Port 80 weitergeleitet.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Die einfachste Einrichtung, es wird keine weitere Komponente gebraucht.&lt;br /&gt;
* Die App funktioniert überall, ohne dass die Nutzer etwas tun müssen.&lt;br /&gt;
* Der REST-Server sieht die IP-Adresse jedes Geräts. Alle Schutzmechanismen des REST-Servers, auch Host-Beschränkung und Rate-Limit, arbeiten damit je Gerät.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Der Server des REST-Dienstes ist direkt aus dem Internet erreichbar. Läuft der REST-Dienst auf dem OBS-Server, gilt das für den OBS-Server und häufig auch für die Datenbank. Die [[#Schutz durch den REST-Server|Schutzmechanismen des REST-Servers]] greifen, eine zusätzliche Schicht davor gibt es aber nicht.&lt;br /&gt;
* Mit der Prüfart http-01 ist zusätzlich Port 80 weitergeleitet. Mit der Prüfart dns-01 entfällt das.&lt;br /&gt;
&lt;br /&gt;
== Reverse-Proxy ==&lt;br /&gt;
Ein Reverse-Proxy, zum Beispiel in einer DMZ oder als Funktion der Firewall, nimmt HTTPS auf Port 443 an. Er prüft die Anfrage und reicht sie über eine eigene, ebenfalls verschlüsselte Verbindung an den REST-Server weiter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Der Server des REST-Dienstes ist aus dem Internet nicht erreichbar, nur der Proxy.&lt;br /&gt;
* Am Proxy lassen sich Anfragen nach Ihren Vorgaben filtern, drosseln und protokollieren.&lt;br /&gt;
* Für die Nutzer der App ändert sich nichts.&lt;br /&gt;
* Das öffentliche Zertifikat liegt am Proxy. Für die Strecke vom Proxy genügt im REST-Server ein Zertifikat Ihrer internen Zertifizierungsstelle oder ein selbst signiertes. Es muss vor Ablauf getauscht werden.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Der Proxy muss betrieben und gepflegt werden.&lt;br /&gt;
* Der REST-Server sieht als Absender nur den Proxy. Die Host-Beschränkung eines Zugangs kann keine Geräte unterscheiden, und Anfragen ohne API-Key teilen sich einen Zähler des Rate-Limits. Die IP-Adresse des Geräts steht nur im Protokoll, wenn der Proxy den Header &#039;&#039;X-Forwarded-For&#039;&#039; setzt.&lt;br /&gt;
&lt;br /&gt;
Die Einrichtung beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung#Betrieb hinter einem Reverse-Proxy|Betrieb hinter einem Reverse-Proxy]].&lt;br /&gt;
&lt;br /&gt;
== VPN ==&lt;br /&gt;
Die Geräte verbinden sich per VPN mit dem Netz, in dem der REST-Dienst läuft. Die App spricht durch den Tunnel per HTTPS mit dem REST-Server. Aus dem Internet ist nur das VPN-Gateway erreichbar.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Für den REST-Server ist kein Port aus dem Internet offen.&lt;br /&gt;
* Nur Geräte mit VPN-Zugang erreichen den REST-Server.&lt;br /&gt;
* Setzt das VPN-Gateway die Adressen nicht um, sieht der REST-Server die VPN-Adresse jedes Geräts.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Jedes Gerät braucht einen VPN-Client und einen eigenen Zugang. Ohne zentrale Geräteverwaltung schalten die Nutzer das VPN von Hand ein. Ist es aus, meldet die App, dass der Server nicht erreichbar ist.&lt;br /&gt;
* Ein VPN-Zugang öffnet den Weg ins Netz. Beschränken Sie ihn auf die Adresse und den Port des REST-Servers.&lt;br /&gt;
* Das Zertifikat lässt sich nur mit der Prüfart dns-01 erneuern, weil die Zertifizierungsstelle den Server nicht erreicht.&lt;br /&gt;
* Der Name des REST-Servers muss auf dem Gerät auf die interne Adresse zeigen. Das geht über einen DNS-Server, den das VPN mitliefert, oder über einen öffentlichen DNS-Eintrag mit der internen Adresse. Manche Router verwerfen solche Antworten als Schutz gegen DNS-Rebinding; dann löst der Name in diesem Netz nicht auf.&lt;br /&gt;
&lt;br /&gt;
Das Zertifikat richten Sie wie unter [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]] beschrieben mit der Prüfart dns-01 ein.&lt;br /&gt;
&lt;br /&gt;
== REST-Dienst auf einem eigenen Server ==&lt;br /&gt;
Der REST-Dienst muss nicht auf dem OBS-Server laufen. Er lässt sich auch auf einem eigenen Server betreiben, zum Beispiel in einer DMZ. Voraussetzung ist, dass dieser Server&lt;br /&gt;
* per SMB auf den OBS-Server zugreifen kann und&lt;br /&gt;
* die Datenbank erreicht.&lt;br /&gt;
&lt;br /&gt;
Der eigene Server lässt sich mit jeder der drei Varianten kombinieren. Bei der direkten Portfreigabe zeigt die Weiterleitung dann auf diesen Server statt auf den OBS-Server.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Eine Verbindung aus dem Internet erreicht weder den OBS-Server noch die Datenbank direkt, auch bei direkter Portfreigabe.&lt;br /&gt;
* Die Verarbeitung der Anfragen läuft nicht auf dem OBS-Server. Die Datenbank wird weiterhin belastet.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Ein weiterer Server muss betrieben und gepflegt werden.&lt;br /&gt;
* Vom eigenen Server ins interne Netz müssen SMB und der Port der Datenbank offen sein. Beschränken Sie diese Wege auf den OBS-Server und den Datenbankserver.&lt;br /&gt;
&lt;br /&gt;
== Entscheidungshilfe ==&lt;br /&gt;
* &#039;&#039;&#039;Keine eingehende Verbindung zum OBS-Server erlaubt, DMZ oder Firewall mit Proxy-Funktion vorhanden:&#039;&#039;&#039; Reverse-Proxy.&lt;br /&gt;
* &#039;&#039;&#039;Keine eingehende Verbindung zum OBS-Server erlaubt, aber kein Proxy vorhanden:&#039;&#039;&#039; REST-Dienst auf einem eigenen Server in einer DMZ, mit direkter Portfreigabe auf diesen Server.&lt;br /&gt;
* &#039;&#039;&#039;Geräte werden zentral verwaltet oder ein VPN ist ohnehin im Einsatz:&#039;&#039;&#039; VPN.&lt;br /&gt;
* &#039;&#039;&#039;Kleine Installation ohne Vorgaben der IT:&#039;&#039;&#039; Direkte Portfreigabe, möglichst mit der Prüfart dns-01, damit Port 80 geschlossen bleibt.&lt;br /&gt;
&lt;br /&gt;
== Siehe auch ==&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge#Host-Beschränkung|Host-Beschränkung]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zertifikatserneuerung&amp;diff=67081</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zertifikatserneuerung&amp;diff=67081"/>
		<updated>2026-10-26T10:31:32Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Reverse-Proxy: selbst signiertes Zertifikat für die Strecke zum REST-Server genannt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
{{Seitentyp|Anleitung}}&lt;br /&gt;
= Zertifikatserneuerung =&lt;br /&gt;
&lt;br /&gt;
Der REST-Server kann sein TLS-Zertifikat selbst bei der Zertifizierungsstelle Let&#039;s Encrypt beantragen und vor Ablauf erneuern. Zertifikat und privater Schlüssel entstehen auf Ihrem Server und verlassen ihn nicht. Diese Seite beschreibt, wie Sie die Erneuerung je nach Netzaufbau einrichten.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Jedes Server-Profil braucht im Betrieb ein Zertifikat, auch hinter einem Reverse-Proxy und im VPN: Eine App verbindet sich ausschliesslich per HTTPS, und ohne Zertifikat startet das Profil nicht. &#039;&#039;kein SSL&#039;&#039; ist nur für lokale Tests gedacht. Das Zertifikat beziehen Sie entweder automatisch wie hier beschrieben, oder Sie hinterlegen es von Hand im Server-Profil und tauschen es vor Ablauf.}}&lt;br /&gt;
&lt;br /&gt;
{{Auf einen Blick|1=&lt;br /&gt;
* &#039;&#039;&#039;Zwei Prüfarten.&#039;&#039;&#039; http-01 prüft über Port 80, dns-01 über einen DNS-Eintrag und braucht keinen offenen Port.&lt;br /&gt;
* &#039;&#039;&#039;Selbsttest vorab.&#039;&#039;&#039; Vor dem ersten Zertifikat und danach wöchentlich prüft der Dienst den Weg über das Testsystem der Zertifizierungsstelle.&lt;br /&gt;
* &#039;&#039;&#039;Erneuerung ab 30 Tagen Restlaufzeit.&#039;&#039;&#039; Das bisherige Zertifikat bleibt als Rückfall erhalten.&lt;br /&gt;
* &#039;&#039;&#039;dns-01 braucht nur ausgehende Verbindungen&#039;&#039;&#039; und eignet sich damit für VPN und für Server ohne Portfreigabe.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Voraussetzungen ==&lt;br /&gt;
* Ein Name, auf den das Zertifikat lautet, zum Beispiel &#039;&#039;obs.firma.de&#039;&#039;. Platzhalter-Zertifikate (&#039;&#039;*.firma.de&#039;&#039;) werden nicht unterstützt.&lt;br /&gt;
* Ein Server-Profil mit TLS, also ohne Haken &#039;&#039;&#039;kein SSL&#039;&#039;&#039;.&lt;br /&gt;
* Ausgehende HTTPS-Verbindungen zur Zertifizierungsstelle (&amp;lt;code&amp;gt;acme-v02.api.letsencrypt.org&amp;lt;/code&amp;gt; und für den Selbsttest &amp;lt;code&amp;gt;acme-staging-v02.api.letsencrypt.org&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Bei dns-01 zusätzlich ausgehende HTTPS-Verbindungen zur Schnittstelle des DNS-Anbieters bzw. zur acme-dns-Instanz.&lt;br /&gt;
&lt;br /&gt;
=== Welche Prüfart? ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Prüfart !! Funktioniert, wenn !! Aus dem Internet erreichbar sein muss&lt;br /&gt;
|-&lt;br /&gt;
| http-01 || der Name öffentlich auf diesen Server zeigt und Port 80 von aussen auf ihn weitergeleitet ist || Port 80&lt;br /&gt;
|-&lt;br /&gt;
| dns-01 || sich der DNS-Eintrag &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; über eine Schnittstelle schreiben lässt, direkt oder über einen CNAME || nichts&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Welche Prüfart zu Ihrer [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|öffentlichen Freigabe]] passt:&lt;br /&gt;
* &#039;&#039;&#039;Direkte Portfreigabe:&#039;&#039;&#039; http-01 oder dns-01. Mit dns-01 bleibt Port 80 geschlossen.&lt;br /&gt;
* &#039;&#039;&#039;Reverse-Proxy:&#039;&#039;&#039; Das öffentliche Zertifikat liegt am Proxy. Der REST-Server braucht trotzdem ein Zertifikat für die Strecke vom Proxy. Dafür genügt ein selbst signiertes Zertifikat oder eines Ihrer internen Zertifizierungsstelle, sofern der Proxy ihm vertraut. Es wird im Server-Profil hinterlegt und muss vor Ablauf getauscht werden.&lt;br /&gt;
* &#039;&#039;&#039;VPN:&#039;&#039;&#039; dns-01.&lt;br /&gt;
&lt;br /&gt;
=== Wege bei dns-01 ===&lt;br /&gt;
Bei dns-01 fragt die Zertifizierungsstelle den TXT-Eintrag &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; ab. Der Dienst schreibt ihn vor der Prüfung und löscht ihn danach wieder. Dafür gibt es drei Wege:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Weg !! Wann !! Einmalig beim DNS des Namens !! Einstellung &#039;&#039;&#039;DNS-Anbieter&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Direkt beim Anbieter || Die Zone des Namens liegt bei Hetzner, Cloudflare oder IONOS || nichts, Sie brauchen nur ein API-Token || &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| CNAME auf eine Zone bei einem Anbieter || Die Zone des Namens liegt bei einem anderen Anbieter, oder im REST-Server sollen keine Zugangsdaten zur eigenen Zone liegen || CNAME &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf einen Namen in einer Zone bei Hetzner, Cloudflare oder IONOS || &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| CNAME auf acme-dns || Eine acme-dns-Instanz steht zur Verfügung, im eigenen Betrieb oder bei einem Dienstleister || CNAME &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf die &#039;&#039;fulldomain&#039;&#039; des acme-dns-Kontos || &#039;&#039;acme-dns&#039;&#039;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim CNAME-Weg genügt beim DNS-Anbieter des Namens ein von Hand angelegter Eintrag, auch bei Anbietern ohne Schnittstelle. Der REST-Server bekommt dann nur Zugang zur Zone, auf die der CNAME zeigt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Nutzen Sie einen DNS-Anbieter, der hier nicht aufgeführt ist, wenden Sie sich an den OBS-Support. Bietet der Anbieter eine Schnittstelle (API) für DNS-Einträge, ist eine Anbindung in der Regel möglich. Bis dahin funktioniert der CNAME-Weg mit jedem Anbieter.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Namen von DynDNS-Diensten (zum Beispiel &#039;&#039;…myfritz.net&#039;&#039;) eignen sich in der Regel nicht als Zertifikatsname, weil sich dort keine eigenen Einträge anlegen lassen. Verwenden Sie einen Namen unter Ihrer eigenen Domain. Der DynDNS-Name bleibt davon unberührt, zum Beispiel für den Zugang zum VPN.}}&lt;br /&gt;
&lt;br /&gt;
== Vorgehen ==&lt;br /&gt;
=== Gemeinsame Einstellungen ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Wählen Sie &#039;&#039;&#039;Stammdaten → Z Weitere Stammdaten → REST-Server → Server&#039;&#039;&#039; und öffnen Sie das Server-Profil.&lt;br /&gt;
# Stellen Sie auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039;.&lt;br /&gt;
# Tragen Sie im Feld &#039;&#039;&#039;Zertifikatsname (FQDN)&#039;&#039;&#039; den Namen ein, auf den das Zertifikat lauten soll.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Kontakt bei der CA&#039;&#039;&#039; eine E-Mail-Adresse ein. Die Zertifizierungsstelle schickt dorthin Warnungen, wenn ein Zertifikat abzulaufen droht.&lt;br /&gt;
# Richten Sie die Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; nach einem der folgenden Abschnitte ein und speichern Sie.&lt;br /&gt;
# Starten Sie den REST-Dienst neu.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart http-01 ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Lassen Sie den Namen öffentlich auf die Adresse Ihres Internetanschlusses zeigen.&lt;br /&gt;
# Leiten Sie am Router Port 80 von aussen auf den Server des REST-Dienstes weiter.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; im Feld &#039;&#039;&#039;Prüfart&#039;&#039;&#039; den Eintrag &#039;&#039;http-01 (Port 80)&#039;&#039;. Die übrigen Felder bleiben leer.&lt;br /&gt;
# Geben Sie in der Konsole des REST-Servers &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Port 80 darf auf dem Server des REST-Dienstes von keinem anderen Dienst belegt sein, häufig ist das ein Webserver wie IIS. Der REST-Server öffnet den Port nur für die Dauer der Prüfung und schliesst ihn danach wieder.&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 direkt beim DNS-Anbieter ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Erstellen Sie beim Anbieter der Zone ein API-Token:&lt;br /&gt;
#* &#039;&#039;&#039;Hetzner:&#039;&#039;&#039; ein API-Token des Projekts mit Lese- und Schreibrecht in der Hetzner Console. Zonen in der früheren DNS-Konsole (dns.hetzner.com) werden nicht unterstützt.&lt;br /&gt;
#* &#039;&#039;&#039;Cloudflare:&#039;&#039;&#039; ein API-Token mit der Berechtigung, DNS zu bearbeiten, beschränkt auf die Zone.&lt;br /&gt;
#* &#039;&#039;&#039;IONOS:&#039;&#039;&#039; einen API-Schlüssel. Er besteht aus Präfix und Secret.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; im Feld &#039;&#039;&#039;Prüfart&#039;&#039;&#039; den Eintrag &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und im Feld &#039;&#039;&#039;DNS-Anbieter&#039;&#039;&#039; den Anbieter.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; das Token ein, bei IONOS in der Form &amp;lt;code&amp;gt;Präfix.Secret&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Lassen Sie &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; leer.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 mit CNAME auf eine Zone bei einem DNS-Anbieter ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Legen Sie in einer Zone bei Hetzner, Cloudflare oder IONOS einen Namen für die Prüfung fest, zum Beispiel &#039;&#039;obs-firma.acme.example.de&#039;&#039;. Einen Eintrag müssen Sie dort nicht anlegen.&lt;br /&gt;
# Legen Sie beim DNS-Anbieter Ihres Namens einen CNAME an: &amp;lt;code&amp;gt;_acme-challenge.obs.firma.de&amp;lt;/code&amp;gt; auf &#039;&#039;obs-firma.acme.example.de&#039;&#039;.&lt;br /&gt;
# Erstellen Sie ein API-Token für die Zone, in der das Ziel liegt, wie im vorigen Abschnitt beschrieben.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; die Prüfart &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und den Anbieter dieser Zone.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; das Token ein.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; das Ziel des CNAME ein, hier &#039;&#039;obs-firma.acme.example.de&#039;&#039;.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 mit CNAME auf acme-dns ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; die Prüfart &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und den DNS-Anbieter &#039;&#039;acme-dns&#039;&#039;.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Adresse (acme-dns)&#039;&#039;&#039; die Adresse der Instanz ein. Sie beginnt mit &amp;lt;code&amp;gt;https://&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Lassen Sie &#039;&#039;&#039;Benutzer (acme-dns)&#039;&#039;&#039;, &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; und &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; leer und speichern Sie.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein. Der Dienst legt das Konto bei acme-dns an und trägt Benutzer, Schlüssel und Name selbst ein. Unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; steht danach der CNAME, den Sie anlegen müssen.&lt;br /&gt;
# Legen Sie beim DNS-Anbieter Ihres Namens diesen CNAME an: &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf die genannte &#039;&#039;fulldomain&#039;&#039;.&lt;br /&gt;
# Geben Sie in der Konsole erneut &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Leeren Sie die drei Felder später nicht. Der Dienst würde ein neues Konto mit einer neuen &#039;&#039;fulldomain&#039;&#039; anlegen, und der gesetzte CNAME zeigte ins Leere. Ein bestehendes Konto tragen Sie mit allen drei Angaben ein. Teilweise gefüllte Angaben weist die Maske beim Speichern ab.}}&lt;br /&gt;
&lt;br /&gt;
=== Erstes Zertifikat holen ===&lt;br /&gt;
Ist der Selbsttest bestanden, holt der Dienst das Zertifikat bei der nächsten stündlichen Prüfung. Sofort geht es mit &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; in der Konsole. Bis das erste Zertifikat da ist, kann sich kein Client mit dem Server-Profil verbinden.&lt;br /&gt;
&lt;br /&gt;
=== Testbetrieb ===&lt;br /&gt;
{{Achtung|Im &#039;&#039;&#039;Testbetrieb&#039;&#039;&#039; beantragt der Dienst das Zertifikat beim Testsystem der Zertifizierungsstelle. Einem solchen Zertifikat vertraut &#039;&#039;&#039;kein&#039;&#039;&#039; Client, Anwendungen können sich danach nicht mehr mit diesem Server verbinden. Verwenden Sie den Testbetrieb ausschliesslich auf Testsystemen. Zurück in den Produktivbetrieb kommen Sie, indem Sie das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039; stellen. Der Dienst holt daraufhin sofort ein gültiges Zertifikat, auch wenn das Testzertifikat noch lange läuft.}}&lt;br /&gt;
&lt;br /&gt;
== Ergebnis ==&lt;br /&gt;
Nach bestandenem Selbsttest ist auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; der Haken &#039;&#039;&#039;Selbsttest bestanden&#039;&#039;&#039; gesetzt. Unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; steht &amp;quot;Selbsttest bestanden - von aussen erreichbar&amp;quot; (http-01) bzw. &amp;quot;Selbsttest bestanden - DNS-Eintrag von der CA geprueft&amp;quot; (dns-01). Nach der Ausstellung zeigt &#039;&#039;&#039;Zertifikat gültig bis&#039;&#039;&#039; das Ablaufdatum, und &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; in der Konsole zeigt den Stand je Profil.&lt;br /&gt;
&lt;br /&gt;
Danach läuft die Erneuerung selbständig:&lt;br /&gt;
# &#039;&#039;&#039;Prüfung:&#039;&#039;&#039; Der Dienst sieht stündlich nach, ob etwas ansteht.&lt;br /&gt;
# &#039;&#039;&#039;Selbsttest:&#039;&#039;&#039; Einmal wöchentlich und nach jeder Änderung von Zertifikatsname, Prüfart, DNS-Anbieter oder Name des TXT-Eintrags prüft der Dienst den Weg über das Testsystem der Zertifizierungsstelle. Besteht der Test nicht, beantragt er kein Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Erneuerung:&#039;&#039;&#039; Ab 30 Tagen Restlaufzeit beantragt der Dienst ein neues Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Übernahme:&#039;&#039;&#039; Der Dienst prüft das neue Zertifikat, speichert es und bewahrt das bisherige als Rückfall auf. Anschliessend startet nur das betroffene Server-Profil neu.&lt;br /&gt;
# &#039;&#039;&#039;Rückfall:&#039;&#039;&#039; Startet das Profil mit dem neuen Zertifikat nicht, aktiviert der Dienst wieder das vorherige.&lt;br /&gt;
&lt;br /&gt;
Nach einem Fehlschlag wartet der Dienst zunehmend länger, bevor er es erneut versucht: eine, zwei, vier, acht und höchstens 24 Stunden. Das Feld &#039;&#039;&#039;Gesperrt bis&#039;&#039;&#039; zeigt, bis wann. &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; in der Konsole wirken sofort, auch während der Sperre.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; || Zeigt je Profil Betriebsart, Ablaufdatum, Zustand und das Ergebnis des letzten Selbsttests&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; || Prüft sofort den Weg über das Testsystem der Zertifizierungsstelle. Beantragt kein Zertifikat&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; || Stösst die Erneuerung sofort an, auch wenn sie noch nicht fällig ist&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Bei dns-01 wartet der Dienst nach dem Schreiben des Eintrags 30 Sekunden und dann bis zu drei Minuten, bis der Eintrag über öffentliche DNS-Server sichtbar ist. Erst dann löst er die Prüfung aus.&lt;br /&gt;
&lt;br /&gt;
== Wenn es nicht klappt ==&lt;br /&gt;
Die Meldungen stehen unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; und im Protokoll des REST-Servers.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Meldung !! Ursache&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Port 80 laesst sich nicht oeffnen - er ist belegt&amp;quot; || Ein anderer Dienst auf dem Server des REST-Dienstes belegt Port 80, häufig IIS. Dienst abschalten oder die Prüfart dns-01 verwenden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;dns-01 ist eingestellt, aber kein DNS-Anbieter&amp;quot; || Auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; ist kein DNS-Anbieter gewählt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;kein API-Token&amp;quot; || Das Feld &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; ist leer&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;acme-dns: Konto unvollstaendig&amp;quot; || Von Benutzer, Schlüssel und Name des TXT-Eintrags ist nur ein Teil gefüllt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Hetzner: keine Zone zu … im Projekt des Tokens&amp;quot; || Das Token gehört zu einem anderen Projekt, oder die Zone liegt noch in der früheren DNS-Konsole&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Cloudflare: keine Zone zu …, die das Token sehen darf&amp;quot; || Das Token ist nicht für diese Zone freigegeben&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;IONOS: keine Zone zu … im Vertrag des Schluessels&amp;quot; || Der API-Schlüssel gehört zu einem anderen Vertrag&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;… lehnt den TXT-Eintrag ab (HTTP 401)&amp;quot; oder &amp;quot;(HTTP 403)&amp;quot; || Das Token ist ungültig oder darf keine DNS-Einträge ändern&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;… ist nach 210 Sekunden noch nicht ueberall sichtbar&amp;quot; || Der Eintrag ist über öffentliche DNS-Server nicht zu sehen. Meist fehlt der CNAME oder zeigt auf ein anderes Ziel als &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039;. Der Dienst löst die Prüfung trotzdem aus&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;kein oeffentlicher DNS-Server erreichbar&amp;quot; || Ausgehendes DNS (Port 53) ist gesperrt. Der Dienst kann die Sichtbarkeit nicht prüfen und wartet fest 60 Sekunden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;acme-dns-Konto angelegt - jetzt CNAME … setzen&amp;quot; || Kein Fehler: Legen Sie den genannten CNAME an und geben Sie &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Siehe auch ==&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile#Automatische Zertifikatserneuerung|Server-Profile: Felder der Zertifikatserneuerung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zertifikatserneuerung&amp;diff=67076</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zertifikatserneuerung&amp;diff=67076"/>
		<updated>2026-10-26T10:23:15Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Hinweis: nicht aufgeführte DNS-Anbieter über den OBS-Support [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
{{Seitentyp|Anleitung}}&lt;br /&gt;
= Zertifikatserneuerung =&lt;br /&gt;
&lt;br /&gt;
Der REST-Server kann sein TLS-Zertifikat selbst bei der Zertifizierungsstelle Let&#039;s Encrypt beantragen und vor Ablauf erneuern. Zertifikat und privater Schlüssel entstehen auf Ihrem Server und verlassen ihn nicht. Diese Seite beschreibt, wie Sie die Erneuerung je nach Netzaufbau einrichten.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Jedes Server-Profil braucht im Betrieb ein Zertifikat, auch hinter einem Reverse-Proxy und im VPN: Die App verbindet sich ausschliesslich per HTTPS, und ohne Zertifikat startet das Profil nicht. &#039;&#039;kein SSL&#039;&#039; ist nur für lokale Tests gedacht. Das Zertifikat beziehen Sie entweder automatisch wie hier beschrieben, oder Sie hinterlegen es von Hand im Server-Profil und tauschen es vor Ablauf.}}&lt;br /&gt;
&lt;br /&gt;
{{Auf einen Blick|1=&lt;br /&gt;
* &#039;&#039;&#039;Zwei Prüfarten.&#039;&#039;&#039; http-01 prüft über Port 80, dns-01 über einen DNS-Eintrag und braucht keinen offenen Port.&lt;br /&gt;
* &#039;&#039;&#039;Selbsttest vorab.&#039;&#039;&#039; Vor dem ersten Zertifikat und danach wöchentlich prüft der Dienst den Weg über das Testsystem der Zertifizierungsstelle.&lt;br /&gt;
* &#039;&#039;&#039;Erneuerung ab 30 Tagen Restlaufzeit.&#039;&#039;&#039; Das bisherige Zertifikat bleibt als Rückfall erhalten.&lt;br /&gt;
* &#039;&#039;&#039;dns-01 braucht nur ausgehende Verbindungen&#039;&#039;&#039; und eignet sich damit für VPN und für Server ohne Portfreigabe.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Voraussetzungen ==&lt;br /&gt;
* Ein Name, auf den das Zertifikat lautet, zum Beispiel &#039;&#039;obs.firma.de&#039;&#039;. Platzhalter-Zertifikate (&#039;&#039;*.firma.de&#039;&#039;) werden nicht unterstützt.&lt;br /&gt;
* Ein Server-Profil mit TLS, also ohne Haken &#039;&#039;&#039;kein SSL&#039;&#039;&#039;.&lt;br /&gt;
* Ausgehende HTTPS-Verbindungen zur Zertifizierungsstelle (&amp;lt;code&amp;gt;acme-v02.api.letsencrypt.org&amp;lt;/code&amp;gt; und für den Selbsttest &amp;lt;code&amp;gt;acme-staging-v02.api.letsencrypt.org&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Bei dns-01 zusätzlich ausgehende HTTPS-Verbindungen zur Schnittstelle des DNS-Anbieters bzw. zur acme-dns-Instanz.&lt;br /&gt;
&lt;br /&gt;
=== Welche Prüfart? ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Prüfart !! Funktioniert, wenn !! Aus dem Internet erreichbar sein muss&lt;br /&gt;
|-&lt;br /&gt;
| http-01 || der Name öffentlich auf diesen Server zeigt und Port 80 von aussen auf ihn weitergeleitet ist || Port 80&lt;br /&gt;
|-&lt;br /&gt;
| dns-01 || sich der DNS-Eintrag &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; über eine Schnittstelle schreiben lässt, direkt oder über einen CNAME || nichts&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Welche Prüfart zu Ihrer [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|öffentlichen Freigabe]] passt:&lt;br /&gt;
* &#039;&#039;&#039;Direkte Portfreigabe:&#039;&#039;&#039; http-01 oder dns-01. Mit dns-01 bleibt Port 80 geschlossen.&lt;br /&gt;
* &#039;&#039;&#039;Reverse-Proxy:&#039;&#039;&#039; Das öffentliche Zertifikat liegt am Proxy. Der REST-Server braucht trotzdem ein Zertifikat für die Strecke vom Proxy, zum Beispiel von Ihrer internen Zertifizierungsstelle. Es wird im Server-Profil hinterlegt und muss vor Ablauf getauscht werden.&lt;br /&gt;
* &#039;&#039;&#039;VPN:&#039;&#039;&#039; dns-01.&lt;br /&gt;
&lt;br /&gt;
=== Wege bei dns-01 ===&lt;br /&gt;
Bei dns-01 fragt die Zertifizierungsstelle den TXT-Eintrag &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; ab. Der Dienst schreibt ihn vor der Prüfung und löscht ihn danach wieder. Dafür gibt es drei Wege:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Weg !! Wann !! Einmalig beim DNS des Namens !! Einstellung &#039;&#039;&#039;DNS-Anbieter&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Direkt beim Anbieter || Die Zone des Namens liegt bei Hetzner, Cloudflare oder IONOS || nichts, Sie brauchen nur ein API-Token || &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| CNAME auf eine Zone bei einem Anbieter || Die Zone des Namens liegt bei einem anderen Anbieter, oder im REST-Server sollen keine Zugangsdaten zur eigenen Zone liegen || CNAME &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf einen Namen in einer Zone bei Hetzner, Cloudflare oder IONOS || &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| CNAME auf acme-dns || Eine acme-dns-Instanz steht zur Verfügung, im eigenen Betrieb oder bei einem Dienstleister || CNAME &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf die &#039;&#039;fulldomain&#039;&#039; des acme-dns-Kontos || &#039;&#039;acme-dns&#039;&#039;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim CNAME-Weg genügt beim DNS-Anbieter des Namens ein von Hand angelegter Eintrag, auch bei Anbietern ohne Schnittstelle. Der REST-Server bekommt dann nur Zugang zur Zone, auf die der CNAME zeigt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Nutzen Sie einen DNS-Anbieter, der hier nicht aufgeführt ist, wenden Sie sich an den OBS-Support. Bietet der Anbieter eine Schnittstelle (API) für DNS-Einträge, ist eine Anbindung in der Regel möglich. Bis dahin funktioniert der CNAME-Weg mit jedem Anbieter.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Namen von DynDNS-Diensten (zum Beispiel &#039;&#039;…myfritz.net&#039;&#039;) eignen sich in der Regel nicht als Zertifikatsname, weil sich dort keine eigenen Einträge anlegen lassen. Verwenden Sie einen Namen unter Ihrer eigenen Domain. Der DynDNS-Name bleibt davon unberührt, zum Beispiel für den Zugang zum VPN.}}&lt;br /&gt;
&lt;br /&gt;
== Vorgehen ==&lt;br /&gt;
=== Gemeinsame Einstellungen ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Wählen Sie &#039;&#039;&#039;Stammdaten → Z Weitere Stammdaten → REST-Server → Server&#039;&#039;&#039; und öffnen Sie das Server-Profil.&lt;br /&gt;
# Stellen Sie auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039;.&lt;br /&gt;
# Tragen Sie im Feld &#039;&#039;&#039;Zertifikatsname (FQDN)&#039;&#039;&#039; den Namen ein, auf den das Zertifikat lauten soll.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Kontakt bei der CA&#039;&#039;&#039; eine E-Mail-Adresse ein. Die Zertifizierungsstelle schickt dorthin Warnungen, wenn ein Zertifikat abzulaufen droht.&lt;br /&gt;
# Richten Sie die Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; nach einem der folgenden Abschnitte ein und speichern Sie.&lt;br /&gt;
# Starten Sie den REST-Dienst neu.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart http-01 ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Lassen Sie den Namen öffentlich auf die Adresse Ihres Internetanschlusses zeigen.&lt;br /&gt;
# Leiten Sie am Router Port 80 von aussen auf den Server des REST-Dienstes weiter.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; im Feld &#039;&#039;&#039;Prüfart&#039;&#039;&#039; den Eintrag &#039;&#039;http-01 (Port 80)&#039;&#039;. Die übrigen Felder bleiben leer.&lt;br /&gt;
# Geben Sie in der Konsole des REST-Servers &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Port 80 darf auf dem Server des REST-Dienstes von keinem anderen Dienst belegt sein, häufig ist das ein Webserver wie IIS. Der REST-Server öffnet den Port nur für die Dauer der Prüfung und schliesst ihn danach wieder.&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 direkt beim DNS-Anbieter ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Erstellen Sie beim Anbieter der Zone ein API-Token:&lt;br /&gt;
#* &#039;&#039;&#039;Hetzner:&#039;&#039;&#039; ein API-Token des Projekts mit Lese- und Schreibrecht in der Hetzner Console. Zonen in der früheren DNS-Konsole (dns.hetzner.com) werden nicht unterstützt.&lt;br /&gt;
#* &#039;&#039;&#039;Cloudflare:&#039;&#039;&#039; ein API-Token mit der Berechtigung, DNS zu bearbeiten, beschränkt auf die Zone.&lt;br /&gt;
#* &#039;&#039;&#039;IONOS:&#039;&#039;&#039; einen API-Schlüssel. Er besteht aus Präfix und Secret.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; im Feld &#039;&#039;&#039;Prüfart&#039;&#039;&#039; den Eintrag &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und im Feld &#039;&#039;&#039;DNS-Anbieter&#039;&#039;&#039; den Anbieter.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; das Token ein, bei IONOS in der Form &amp;lt;code&amp;gt;Präfix.Secret&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Lassen Sie &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; leer.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 mit CNAME auf eine Zone bei einem DNS-Anbieter ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Legen Sie in einer Zone bei Hetzner, Cloudflare oder IONOS einen Namen für die Prüfung fest, zum Beispiel &#039;&#039;obs-firma.acme.example.de&#039;&#039;. Einen Eintrag müssen Sie dort nicht anlegen.&lt;br /&gt;
# Legen Sie beim DNS-Anbieter Ihres Namens einen CNAME an: &amp;lt;code&amp;gt;_acme-challenge.obs.firma.de&amp;lt;/code&amp;gt; auf &#039;&#039;obs-firma.acme.example.de&#039;&#039;.&lt;br /&gt;
# Erstellen Sie ein API-Token für die Zone, in der das Ziel liegt, wie im vorigen Abschnitt beschrieben.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; die Prüfart &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und den Anbieter dieser Zone.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; das Token ein.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; das Ziel des CNAME ein, hier &#039;&#039;obs-firma.acme.example.de&#039;&#039;.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 mit CNAME auf acme-dns ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; die Prüfart &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und den DNS-Anbieter &#039;&#039;acme-dns&#039;&#039;.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Adresse (acme-dns)&#039;&#039;&#039; die Adresse der Instanz ein. Sie beginnt mit &amp;lt;code&amp;gt;https://&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Lassen Sie &#039;&#039;&#039;Benutzer (acme-dns)&#039;&#039;&#039;, &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; und &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; leer und speichern Sie.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein. Der Dienst legt das Konto bei acme-dns an und trägt Benutzer, Schlüssel und Name selbst ein. Unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; steht danach der CNAME, den Sie anlegen müssen.&lt;br /&gt;
# Legen Sie beim DNS-Anbieter Ihres Namens diesen CNAME an: &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf die genannte &#039;&#039;fulldomain&#039;&#039;.&lt;br /&gt;
# Geben Sie in der Konsole erneut &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Leeren Sie die drei Felder später nicht. Der Dienst würde ein neues Konto mit einer neuen &#039;&#039;fulldomain&#039;&#039; anlegen, und der gesetzte CNAME zeigte ins Leere. Ein bestehendes Konto tragen Sie mit allen drei Angaben ein. Teilweise gefüllte Angaben weist die Maske beim Speichern ab.}}&lt;br /&gt;
&lt;br /&gt;
=== Erstes Zertifikat holen ===&lt;br /&gt;
Ist der Selbsttest bestanden, holt der Dienst das Zertifikat bei der nächsten stündlichen Prüfung. Sofort geht es mit &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; in der Konsole. Bis das erste Zertifikat da ist, kann sich kein Client mit dem Server-Profil verbinden.&lt;br /&gt;
&lt;br /&gt;
=== Testbetrieb ===&lt;br /&gt;
{{Achtung|Im &#039;&#039;&#039;Testbetrieb&#039;&#039;&#039; beantragt der Dienst das Zertifikat beim Testsystem der Zertifizierungsstelle. Einem solchen Zertifikat vertraut &#039;&#039;&#039;kein&#039;&#039;&#039; Client, Anwendungen können sich danach nicht mehr mit diesem Server verbinden. Verwenden Sie den Testbetrieb ausschliesslich auf Testsystemen. Zurück in den Produktivbetrieb kommen Sie, indem Sie das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039; stellen. Der Dienst holt daraufhin sofort ein gültiges Zertifikat, auch wenn das Testzertifikat noch lange läuft.}}&lt;br /&gt;
&lt;br /&gt;
== Ergebnis ==&lt;br /&gt;
Nach bestandenem Selbsttest ist auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; der Haken &#039;&#039;&#039;Selbsttest bestanden&#039;&#039;&#039; gesetzt. Unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; steht &amp;quot;Selbsttest bestanden - von aussen erreichbar&amp;quot; (http-01) bzw. &amp;quot;Selbsttest bestanden - DNS-Eintrag von der CA geprueft&amp;quot; (dns-01). Nach der Ausstellung zeigt &#039;&#039;&#039;Zertifikat gültig bis&#039;&#039;&#039; das Ablaufdatum, und &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; in der Konsole zeigt den Stand je Profil.&lt;br /&gt;
&lt;br /&gt;
Danach läuft die Erneuerung selbständig:&lt;br /&gt;
# &#039;&#039;&#039;Prüfung:&#039;&#039;&#039; Der Dienst sieht stündlich nach, ob etwas ansteht.&lt;br /&gt;
# &#039;&#039;&#039;Selbsttest:&#039;&#039;&#039; Einmal wöchentlich und nach jeder Änderung von Zertifikatsname, Prüfart, DNS-Anbieter oder Name des TXT-Eintrags prüft der Dienst den Weg über das Testsystem der Zertifizierungsstelle. Besteht der Test nicht, beantragt er kein Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Erneuerung:&#039;&#039;&#039; Ab 30 Tagen Restlaufzeit beantragt der Dienst ein neues Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Übernahme:&#039;&#039;&#039; Der Dienst prüft das neue Zertifikat, speichert es und bewahrt das bisherige als Rückfall auf. Anschliessend startet nur das betroffene Server-Profil neu.&lt;br /&gt;
# &#039;&#039;&#039;Rückfall:&#039;&#039;&#039; Startet das Profil mit dem neuen Zertifikat nicht, aktiviert der Dienst wieder das vorherige.&lt;br /&gt;
&lt;br /&gt;
Nach einem Fehlschlag wartet der Dienst zunehmend länger, bevor er es erneut versucht: eine, zwei, vier, acht und höchstens 24 Stunden. Das Feld &#039;&#039;&#039;Gesperrt bis&#039;&#039;&#039; zeigt, bis wann. &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; in der Konsole wirken sofort, auch während der Sperre.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; || Zeigt je Profil Betriebsart, Ablaufdatum, Zustand und das Ergebnis des letzten Selbsttests&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; || Prüft sofort den Weg über das Testsystem der Zertifizierungsstelle. Beantragt kein Zertifikat&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; || Stösst die Erneuerung sofort an, auch wenn sie noch nicht fällig ist&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Bei dns-01 wartet der Dienst nach dem Schreiben des Eintrags 30 Sekunden und dann bis zu drei Minuten, bis der Eintrag über öffentliche DNS-Server sichtbar ist. Erst dann löst er die Prüfung aus.&lt;br /&gt;
&lt;br /&gt;
== Wenn es nicht klappt ==&lt;br /&gt;
Die Meldungen stehen unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; und im Protokoll des REST-Servers.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Meldung !! Ursache&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Port 80 laesst sich nicht oeffnen - er ist belegt&amp;quot; || Ein anderer Dienst auf dem Server des REST-Dienstes belegt Port 80, häufig IIS. Dienst abschalten oder die Prüfart dns-01 verwenden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;dns-01 ist eingestellt, aber kein DNS-Anbieter&amp;quot; || Auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; ist kein DNS-Anbieter gewählt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;kein API-Token&amp;quot; || Das Feld &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; ist leer&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;acme-dns: Konto unvollstaendig&amp;quot; || Von Benutzer, Schlüssel und Name des TXT-Eintrags ist nur ein Teil gefüllt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Hetzner: keine Zone zu … im Projekt des Tokens&amp;quot; || Das Token gehört zu einem anderen Projekt, oder die Zone liegt noch in der früheren DNS-Konsole&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Cloudflare: keine Zone zu …, die das Token sehen darf&amp;quot; || Das Token ist nicht für diese Zone freigegeben&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;IONOS: keine Zone zu … im Vertrag des Schluessels&amp;quot; || Der API-Schlüssel gehört zu einem anderen Vertrag&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;… lehnt den TXT-Eintrag ab (HTTP 401)&amp;quot; oder &amp;quot;(HTTP 403)&amp;quot; || Das Token ist ungültig oder darf keine DNS-Einträge ändern&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;… ist nach 210 Sekunden noch nicht ueberall sichtbar&amp;quot; || Der Eintrag ist über öffentliche DNS-Server nicht zu sehen. Meist fehlt der CNAME oder zeigt auf ein anderes Ziel als &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039;. Der Dienst löst die Prüfung trotzdem aus&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;kein oeffentlicher DNS-Server erreichbar&amp;quot; || Ausgehendes DNS (Port 53) ist gesperrt. Der Dienst kann die Sichtbarkeit nicht prüfen und wartet fest 60 Sekunden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;acme-dns-Konto angelegt - jetzt CNAME … setzen&amp;quot; || Kein Fehler: Legen Sie den genannten CNAME an und geben Sie &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Siehe auch ==&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile#Automatische Zertifikatserneuerung|Server-Profile: Felder der Zertifikatserneuerung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=67079</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Einrichtung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=67079"/>
		<updated>2026-10-26T10:21:58Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Reverse-Proxy: internes Zertifikat läuft ab und muss getauscht werden [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Einrichtung eines REST-Endpunktes=&lt;br /&gt;
&lt;br /&gt;
Die folgende Reihenfolge stellt sicher, dass ein lauffähiger Endpunkt mit möglichst wenigen Korrektur-Schleifen entsteht.&lt;br /&gt;
&lt;br /&gt;
==Vorbereitung==&lt;br /&gt;
&lt;br /&gt;
# OBS-Support kontaktieren und das REST-Server-Modul aktivieren lassen.&lt;br /&gt;
# Prüfen, ob auf dem Zielrechner (OBS-Server oder dedizierter REST-Server) Port und IP-Adresse frei sind.&lt;br /&gt;
# Bei TLS-Profilen: TLS-Zertifikat (PEM, mit Kette), privaten Schlüssel (PEM) und - falls mTLS - das CA-Root-Zertifikat bereithalten. Alternativ nutzen Sie die [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|automatische Zertifikatserneuerung]]. Wie der Server von aussen erreichbar wird und welche Variante sich eignet, beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 1: Server-Profil anlegen==&lt;br /&gt;
&lt;br /&gt;
In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server&#039;&#039;&#039; den Punkt &#039;&#039;&#039;Server&#039;&#039;&#039; öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; ein neues Profil anlegen. Pflichtfelder:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Nr&#039;&#039;&#039; - eindeutige Nummer (1-99), wird beim Anlegen automatisch vorgeschlagen.&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - sprechender Name (z.B. &#039;&#039;Public-API&#039;&#039;, &#039;&#039;Internal-mTLS&#039;&#039;, &#039;&#039;Debug-Local&#039;&#039;).&lt;br /&gt;
* Auswahl des Modus:&lt;br /&gt;
** &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039; (Default): Zertifikat und Schlüssel hinterlegen - oder auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; die automatische Erneuerung aktivieren.&lt;br /&gt;
** &#039;&#039;&#039;mTLS-Modus aktiv&#039;&#039;&#039; setzen: zusätzlich das CA-Root-Zertifikat hinterlegen, gegen das die Client-Zertifikate geprüft werden. Jeder Client muss dann ein gültiges Zertifikat vorlegen.&lt;br /&gt;
** &#039;&#039;&#039;kein SSL&#039;&#039;&#039; setzen: ausschliesslich für lokale Tests, der Server akzeptiert dann nur Plain-HTTP.&lt;br /&gt;
&lt;br /&gt;
Optional lassen sich am Profil eigene &#039;&#039;&#039;Rate-Limit-Schwellen&#039;&#039;&#039; hinterlegen. Ohne&lt;br /&gt;
Angabe gelten die Standardwerte (60-Sekunden-Fenster, 120 erfolgreiche und 10&lt;br /&gt;
fehlgeschlagene Anfragen je Schlüssel, 600 Anfragen je Profil). Für Anwendungen&lt;br /&gt;
ohne eigenen API-Key - typisch mobile Apps - sollten die Werte bewusst gesetzt&lt;br /&gt;
werden, weil dann alle Benutzer hinter einer IP auf denselben Zähler laufen.&lt;br /&gt;
&lt;br /&gt;
Details siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 2: Bindung anlegen==&lt;br /&gt;
&lt;br /&gt;
Im Server-Profil mit &#039;&#039;&#039;F6&#039;&#039;&#039; die Bindungs-Liste öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; eine neue Bindung anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - IP-Adresse, auf der gelauscht werden soll (z.B. &#039;&#039;0.0.0.0&#039;&#039; für alle Adressen, &#039;&#039;127.0.0.1&#039;&#039; für lokal).&lt;br /&gt;
* &#039;&#039;&#039;Port&#039;&#039;&#039; - Port-Nummer (typisch 443 für HTTPS, 8099 für Debug, ggf. abweichend). {{Achtung|Port 8080 wird für RustDesk verwendet und darf daher nicht verwendet werden}}&lt;br /&gt;
* &#039;&#039;&#039;Standard&#039;&#039;&#039; setzen, wenn diese Bindung die Standard-Bindung des Servers ist (pro Server-Profil nur eine).&lt;br /&gt;
&lt;br /&gt;
==Schritt 3: REST-Dienst starten / neustarten==&lt;br /&gt;
&lt;br /&gt;
Damit Änderungen an Server-Profilen oder Bindungen wirksam werden, muss der OBS REST-Server-Dienst (oder die Konsole) gestartet bzw. neugestartet werden. Das Profil wird beim Start aus der Datenbank gelesen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 4: Zugang anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Zugänge&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Zugang anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - erscheint im Protokoll und in der Statistik.&lt;br /&gt;
* &#039;&#039;&#039;API-Key&#039;&#039;&#039; - eindeutig, mit dem Schlüsselsymbol kann ein zufälliger Key generiert werden.&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - optional die IP oder der Hostname des Konsumenten (sollte, wenn möglich, immer gesetzt werden).&lt;br /&gt;
* Optional: &#039;&#039;&#039;CORS-Origins&#039;&#039;&#039; und &#039;&#039;&#039;JWT-Konfiguration&#039;&#039;&#039;, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 5: Endpunkt anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Endpunkte&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Endpunkt anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Endpunkt&#039;&#039;&#039; - Ressourcen-/Anzeigename (z.B. &#039;&#039;orders&#039;&#039;, &#039;&#039;kalender&#039;&#039;); dient nur der Übersicht, nicht dem Routing.&lt;br /&gt;
* &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; - der maßgebliche Routing-Pfad, ggf. mit Platzhaltern (z.B. &#039;&#039;/orders&#039;&#039;, &#039;&#039;/orders/{uid}&#039;&#039;, &#039;&#039;/orders/{uid}/modules/{code}&#039;&#039;).&lt;br /&gt;
* &#039;&#039;&#039;Server&#039;&#039;&#039; - die Server-Auswahl bestimmt, über welches Server-Profil der Endpunkt erreichbar ist.&lt;br /&gt;
* &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; - nur setzen, wenn der Endpunkt Dateien entgegennehmen soll; dann auch die &#039;&#039;&#039;Max. Upload-Grösse&#039;&#039;&#039; in MB festlegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6]]).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Eine Versionskennung wird als statisches Segment ins Pfad-Template aufgenommen (z.B. &#039;&#039;/orders/v1&#039;&#039;); ein eigenes Versionsfeld gibt es nicht. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].}}&lt;br /&gt;
&lt;br /&gt;
Anschliessend mit &#039;&#039;&#039;F7&#039;&#039;&#039; das Endpunkt-Skript pflegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]]).&lt;br /&gt;
&lt;br /&gt;
==Schritt 6: Zugang für den Endpunkt freischalten==&lt;br /&gt;
&lt;br /&gt;
Aus der Endpunkt-Liste den neuen Endpunkt markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die Berechtigungs-Liste. Mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; kann ein oder mehrere Zugänge ausgewählt und mit &#039;&#039;&#039;F2&#039;&#039;&#039; übernommen werden. Erst nach dieser Freischaltung darf ein Zugang den Endpunkt aufrufen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 7: Funktionsprüfung==&lt;br /&gt;
&lt;br /&gt;
Aufruf mit einem HTTP-Client (curl, Postman, Browser-Plug-in):&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; https://api.meinserver.de/[Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Bei korrektem Setup erscheint die JSON-Antwort des Skripts. Fehlerfälle werden in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; protokolliert.&lt;br /&gt;
&lt;br /&gt;
==Schritt 8: Statistik und Protokoll prüfen==&lt;br /&gt;
&lt;br /&gt;
* In der Endpunkt-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Endpunkts.&lt;br /&gt;
* In der Zugänge-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Zugangs.&lt;br /&gt;
* In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Protokoll&#039;&#039;&#039; die Ereignisse prüfen.&lt;br /&gt;
&lt;br /&gt;
==Betrieb hinter einem Reverse-Proxy==&lt;br /&gt;
&lt;br /&gt;
Soll der Server des REST-Dienstes nicht direkt aus dem Internet erreichbar sein, nimmt ein Reverse-Proxy die Anfragen entgegen, zum Beispiel in einer DMZ oder als Funktion der Firewall, und reicht sie an den REST-Dienst weiter. Von aussen ist nur der Proxy erreichbar. Vor- und Nachteile gegenüber Portfreigabe und VPN beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]].&lt;br /&gt;
&lt;br /&gt;
Am Proxy und im Netz richten Sie ein:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Öffentlicher Name:&#039;&#039;&#039; zeigt auf die öffentliche IP-Adresse des Proxys.&lt;br /&gt;
* &#039;&#039;&#039;Zertifikat:&#039;&#039;&#039; Der Proxy braucht ein öffentlich gültiges Zertifikat für diesen Namen. Die automatische Zertifikatserneuerung des REST-Servers wird nicht benötigt.&lt;br /&gt;
* &#039;&#039;&#039;Weiterleitung:&#039;&#039;&#039; Der Proxy leitet per HTTPS an &amp;lt;code&amp;gt;https://&amp;lt;Server des REST-Dienstes&amp;gt;:&amp;lt;Port&amp;gt;&amp;lt;/code&amp;gt; weiter. Für diese Strecke genügt am REST-Server ein Zertifikat Ihrer internen Zertifizierungsstelle oder ein selbst signiertes, dem der Proxy vertraut. Auch dieses Zertifikat läuft ab und muss vor Ablauf getauscht werden.&lt;br /&gt;
* &#039;&#039;&#039;Header:&#039;&#039;&#039; Alle HTTP-Methoden und Header unverändert durchreichen, insbesondere &#039;&#039;Authorization&#039;&#039;, &#039;&#039;apikey&#039;&#039; und &#039;&#039;Idempotency-Key&#039;&#039;. Antworten nicht zwischenspeichern. Den Header &#039;&#039;X-Forwarded-For&#039;&#039; mit der IP-Adresse des Konsumenten setzen.&lt;br /&gt;
* &#039;&#039;&#039;Anfragegrösse:&#039;&#039;&#039; Die maximale Anfragegrösse am Proxy darf nicht unter der Upload-Grenze der Endpunkte liegen.&lt;br /&gt;
* &#039;&#039;&#039;Firewall:&#039;&#039;&#039; Den Port des REST-Servers intern nur für den Proxy freigeben.&lt;br /&gt;
&lt;br /&gt;
Hinter dem Proxy verhält sich der REST-Server so:&lt;br /&gt;
&lt;br /&gt;
* Protokoll und Konsole zeigen als Host die IP-Adresse des Proxys und hängen den Wert von &#039;&#039;X-Forwarded-For&#039;&#039; als &amp;lt;code&amp;gt;[xff=…]&amp;lt;/code&amp;gt; an.&lt;br /&gt;
* Die [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge#Host-Beschränkung|Host-Beschränkung]] eines Zugangs kann keine einzelnen Konsumenten unterscheiden.&lt;br /&gt;
* Beim [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile#Rate-Limit je Profil|Rate-Limit]] teilen sich alle Konsumenten ohne API-Key einen Zähler.&lt;br /&gt;
&lt;br /&gt;
==Wartung der Server-Tabellen==&lt;br /&gt;
&lt;br /&gt;
Die Tabellen des REST-Servers pflegen sich selbst; ein regelmässiger Eingriff ist&lt;br /&gt;
nicht vorgesehen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Tabelle !! Aufräumen&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_IDEMPOTENCY || Abgeschlossene Einträge werden nach 30 Tagen gelöscht. Offene Reservierungen werden ab 24 Stunden im Protokoll gemeldet und &#039;&#039;&#039;anschliessend ebenfalls gelöscht&#039;&#039;&#039; - sie sind trotzdem ein Support-Fall, weil die Fachwirkung unbekannt ist, siehe Stolpersteine&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_TOKEN || Zeilen werden gelöscht, sobald Access- &#039;&#039;&#039;und&#039;&#039;&#039; Refresh-Laufzeit vorbei sind. Läuft beim Serverstart und danach höchstens stündlich&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_PROTO / RESTSRV_STATS || Wachsen mit dem Betrieb. Bei hohem Aufkommen empfiehlt sich ein turnusmässiges Archivieren&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Häufige Stolpersteine==&lt;br /&gt;
&lt;br /&gt;
* Bindung angelegt, aber der Server wurde nicht neugestartet -&amp;gt; die Bindung ist noch nicht aktiv.&lt;br /&gt;
* Endpunkt einem falschen Server zugeordnet -&amp;gt; Aufruf liefert 404 &#039;&#039;Endpunkt nicht vorhanden oder inaktiv&#039;&#039;.&lt;br /&gt;
* Pfad-Template falsch geschrieben oder Segmentzahl passt nicht -&amp;gt; kein Template matcht, Aufruf liefert 404.&lt;br /&gt;
* Zugang für den Endpunkt nicht freigeschaltet -&amp;gt; Aufruf liefert 403 &#039;&#039;Keine Berechtigung für die Endpunkt-Nutzung&#039;&#039;.&lt;br /&gt;
* Falscher API-Key oder falscher Host beim Zugang -&amp;gt; Aufruf liefert 401 bzw. 403.&lt;br /&gt;
* mTLS aktiv, aber kein Client-Zertifikat installiert -&amp;gt; Verbindung wird während des TLS-Handshakes abgebrochen.&lt;br /&gt;
* Datei-Upload ohne gesetzten Haken &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; am Endpunkt -&amp;gt; Aufruf liefert 415, das Skript läuft nicht an.&lt;br /&gt;
* Skript geändert, aber der Aufruf verhält sich noch wie vorher -&amp;gt; Endpunkt-Cache abwarten (bis zu 60 Sekunden).&lt;br /&gt;
* Schreibaufruf (POST/PUT/PATCH/DELETE) liefert 400 &#039;&#039;IDEMPOTENCY_KEY_MISSING&#039;&#039; -&amp;gt; der Client sendet keinen &#039;&#039;Idempotency-Key&#039;&#039;. Der Header ist seit v1.21 &#039;&#039;&#039;Pflicht&#039;&#039;&#039;, nicht optional; das Endpunkt-Skript läuft in diesem Fall gar nicht an. Betrifft typischerweise Konsumenten, die vor dem Update gebaut wurden.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 422 &#039;&#039;IDEMPOTENCY_KEY_REUSED&#039;&#039; -&amp;gt; der Client sendet denselben &#039;&#039;Idempotency-Key&#039;&#039; mit geändertem Inhalt. Pro Vorgang genau ein Schlüssel, über alle Wiederholversuche hinweg unverändert.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 409 &#039;&#039;IDEMPOTENCY_UNRESOLVED&#039;&#039; -&amp;gt; ein früherer Versuch wurde hart abgebrochen (Dienst beendet). Ob gebucht wurde, ist offen: im Protokoll und in den Fachdaten prüfen. Der Aufräumlauf meldet den Eintrag nach 24 Stunden im Protokoll und löscht ihn dann; wer schneller weitermachen will, entfernt ihn in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;.&lt;br /&gt;
* Nach einem Update antworten &#039;&#039;&#039;alle&#039;&#039;&#039; bestehenden Token mit 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; -&amp;gt; erwartetes Verhalten. Token, die vor der Einführung der Sitzungsprüfung ausgestellt wurden, gehören zu keiner Sitzung; die Konsumenten müssen sich einmalig neu anmelden.&lt;br /&gt;
* Anmeldung liefert 503 statt eines Tokens -&amp;gt; die Sitzungszeile konnte nicht geschrieben werden. Ursache im Protokoll unter &#039;&#039;TokenStore: ...&#039;&#039; nachsehen; typisch ist eine fehlende Tabelle &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; nach einem Update ohne Datenbank-Abgleich.&lt;br /&gt;
* Abmelden liefert 503 -&amp;gt; die Sitzung liess sich nicht sperren. Der Client ist &#039;&#039;&#039;nicht&#039;&#039;&#039; abgemeldet und muss den Aufruf wiederholen; Details im Protokoll.&lt;br /&gt;
* Techniker wird beim Arbeiten unerwartet abgemeldet -&amp;gt; im Protokoll nach &#039;&#039;WIEDERVORLAGE&#039;&#039; suchen. Ein doppelt verwendetes Refresh-Token sperrt die Sitzung. Legt die App Token mehrfach parallel ab, gehört das clientseitig behoben.&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=67078</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Server-Profile</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=67078"/>
		<updated>2026-10-26T10:21:58Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Modi: Standard-TLS gilt für jeden Betrieb, nicht nur im öffentlichen Netz [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Server-Profile=&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Server&#039;&#039;&#039; im REST-Modul ist ein eigenständiges TLS-Profil mit einer eigenen HTTP-Server-Instanz. In OBS können beliebig viele Server-Profile parallel betrieben werden, jedes mit eigenem Modus, eigenen Zertifikaten und eigenen Bindungen.&lt;br /&gt;
&lt;br /&gt;
Typische Einsatzfälle für mehrere Profile:&lt;br /&gt;
&lt;br /&gt;
* Ein &#039;&#039;&#039;Public-Profil&#039;&#039;&#039; mit Standard-TLS auf Port 443 für externe Konsumenten.&lt;br /&gt;
* Ein &#039;&#039;&#039;Internal-mTLS-Profil&#039;&#039;&#039; auf einem internen Port für Microservices, die zwingend ein Client-Zertifikat vorlegen müssen.&lt;br /&gt;
* Ein &#039;&#039;&#039;Debug-Profil&#039;&#039;&#039; mit Plain-HTTP auf 127.0.0.1, ausschliesslich für lokale Tests.&lt;br /&gt;
&lt;br /&gt;
==Aufruf==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Server&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Felder des Server-Profils==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Nr                  || Eindeutige Nummer 1-99, einmalig vergeben, kann später nicht mehr geändert werden&lt;br /&gt;
|-&lt;br /&gt;
| Name                || Sprechender Name, eindeutig - erscheint in Protokoll, Statistik und Endpunkt-Zuordnung&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv               || Profil wird beim Start des REST-Dienstes geladen. Inaktive Profile starten nicht&lt;br /&gt;
|-&lt;br /&gt;
| zusätzliche Infos   || Freitext zur Dokumentation (interner Kommentar, wird nicht ausgewertet)&lt;br /&gt;
|-&lt;br /&gt;
| kein SSL            || Plain-HTTP-Modus, ausschliesslich für Debug. TLS-Felder werden ignoriert&lt;br /&gt;
|-&lt;br /&gt;
| mTLS-Modus aktiv    || Mutual TLS: jeder Client muss ein gültiges Client-Zertifikat vorlegen&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikat (cert)   || PEM-Inhalt des Server-Zertifikats (inkl. ggf. Zwischen-CA-Kette)&lt;br /&gt;
|-&lt;br /&gt;
| Schlüssel (key)     || PEM-Inhalt des privaten Schlüssels&lt;br /&gt;
|-&lt;br /&gt;
| Root-Zertifikat (ca) || PEM-Inhalt des CA-Zertifikats, gegen das &#039;&lt;br /&gt;
|-&lt;br /&gt;
| Rate-Limit-Fenster  || Länge des Rate-Limit-Fensters in Sekunden. Leer/0 = Standard 60&lt;br /&gt;
|-&lt;br /&gt;
| Erfolgsanfragen     || Erlaubte erfolgreiche Anfragen je Schlüssel und Fenster. Leer/0 = Standard 120&lt;br /&gt;
|-&lt;br /&gt;
| Fehlanfragen        || Erlaubte fehlgeschlagene Anfragen je Schlüssel und Fenster. Leer/0 = Standard 10&lt;br /&gt;
|-&lt;br /&gt;
| Anfragen            || Erlaubte Anfragen des gesamten Profils je Fenster. Leer/0 = Standard 600&lt;br /&gt;
|-&lt;br /&gt;
| max. Verbindungen   || Obergrenze gleichzeitiger &#039;&#039;&#039;Verbindungen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_conn&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Sie deckt auch laufende Datei-Downloads ab, weil der Platz erst beim Trennen frei wird. Bei Überschreitung trennt der Server &#039;&#039;&#039;ohne HTTP-Antwort&#039;&#039;&#039; - der Wert gehört deutlich über den Normalbetrieb, als Reissleine&lt;br /&gt;
|-&lt;br /&gt;
| max. Anfragen       || Obergrenze gleichzeitig &#039;&#039;&#039;verarbeiteter Anfragen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_parallel&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Bei Überschreitung antwortet der Server &#039;&#039;&#039;503&#039;&#039;&#039; mit &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;, der Client darf es also gleich erneut versuchen&lt;br /&gt;
|-&lt;br /&gt;
| keine Kompression   || Antworten dieses Profils werden &#039;&#039;&#039;nicht&#039;&#039;&#039; gepackt (&amp;lt;code&amp;gt;rsv_no_gzip&amp;lt;/code&amp;gt;). Ohne Haken packt der Server JSON- und Textantworten ab 1024 Byte mit gzip, sofern der Client sie über &amp;lt;code&amp;gt;Accept-Encoding&amp;lt;/code&amp;gt; annimmt. Der Haken betrifft nur die Antwortrichtung - komprimierte &#039;&#039;&#039;Anfragen&#039;&#039;&#039; nimmt der Server weiterhin entgegen&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;max. Verbindungen&#039;&#039; und &#039;&#039;max. Anfragen&#039;&#039; sind die beiden&lt;br /&gt;
&#039;&#039;&#039;Lastgrenzen&#039;&#039;&#039; des Profils. Davon zu unterscheiden sind die festen Grenzen der&lt;br /&gt;
persistenten Verbindungen - 10 Sekunden Leerlauf, 100 Anfragen je Verbindung -,&lt;br /&gt;
die nicht der Last dienen, sondern der Hygiene und nicht einstellbar sind. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer|REST-Server]], Abschnitt Persistente&lt;br /&gt;
Verbindungen.}}&lt;br /&gt;
&lt;br /&gt;
Die Inhalte der Zertifikate werden direkt in OBS als Text gespeichert. Zur Laufzeit schreibt der Server die PEM-Inhalte für den Bruchteil einer Sekunde in ein Temp-Verzeichnis, damit OpenSSL sie beim Start lesen kann; danach werden die Temp-Dateien sofort wieder gelöscht.&lt;br /&gt;
&lt;br /&gt;
==Modi==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Modus               !! Voraussetzungen                       !! Anwendung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039;  || Zertifikat + Schlüssel                || Für den Betrieb, auch wenn der Server nur intern, hinter einem Reverse-Proxy oder per VPN erreichbar ist&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;mTLS&#039;&#039;&#039;          || Zertifikat + Schlüssel + Root-Zert.   || Maschine-zu-Maschine-Kommunikation, in der jeder Client mit Cert validiert wird&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Plain-HTTP&#039;&#039;&#039;    || Haken &#039;&#039;kein SSL&#039;&#039;                    || Ausschliesslich lokale Tests. Erkennt TLS-Versuche auf dem Plain-Port und trennt sie direkt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beide Richtungen werden erkannt: Eine &#039;&#039;&#039;unverschlüsselte Anfrage an einen&lt;br /&gt;
TLS-Port&#039;&#039;&#039; (&#039;&#039;http&#039;&#039; statt &#039;&#039;https&#039;&#039;) beantwortet der Server mit &#039;&#039;&#039;HTTP 400&#039;&#039;&#039;&lt;br /&gt;
und einem Klartext-Hinweis. Die Abweisung erfolgt &#039;&#039;&#039;vor&#039;&#039;&#039; Authentifizierung,&lt;br /&gt;
Rate-Limit und Endpunkt-Skript - ein falsch konfigurierter Client löst also&lt;br /&gt;
keine Verarbeitung aus.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Bei Standard-TLS und mTLS fährt der Server ohne Zertifikat und Schlüssel gar nicht erst hoch. Bei aktiver Erneuerung trägt der Dienst beides selbst ein. Plain-HTTP umgeht TLS komplett und ist nicht für Produktivumgebungen geeignet.}}&lt;br /&gt;
&lt;br /&gt;
==TLS-Version, Cipher und Zertifikats-Rotation==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TLS-Version:&#039;&#039;&#039; Der Server erzwingt &#039;&#039;&#039;TLS 1.2&#039;&#039;&#039;. Die Cipher-Liste ist auf ECDHE-Schlüsselaustausch (Forward Secrecy) mit AEAD-Verfahren (AES-GCM, ChaCha20-Poly1305) beschränkt; CBC, statisches RSA-KEX und DHE sind ausgeschlossen.&lt;br /&gt;
* &#039;&#039;&#039;HSTS:&#039;&#039;&#039; Jede Antwort eines TLS-Profils trägt &#039;&#039;Strict-Transport-Security: max-age=31536000; includeSubDomains&#039;&#039;. Ein Browser spricht die Adresse damit ein Jahr lang nur noch über HTTPS an. Beim Profil &#039;&#039;Kein SSL&#039;&#039; wird der Header bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; gesetzt - der Browser würde ihn sonst speichern und die Erzwingung auch auf produktive Sitzungen anwenden.&lt;br /&gt;
* &#039;&#039;&#039;Rotation:&#039;&#039;&#039; Bei &#039;&#039;&#039;automatischer Erneuerung&#039;&#039;&#039; tauscht der Dienst das Zertifikat selbst und startet dabei nur das betroffene Server-Profil neu; die übrigen Profile und der Dienst laufen weiter. Bei manueller Pflege ändern Sie die PEM-Inhalte und starten den REST-Dienst neu - bis dahin läuft das alte Zertifikat weiter.&lt;br /&gt;
* &#039;&#039;&#039;Certificate-Pinning:&#039;&#039;&#039; Mobile Clients können den Server-Public-Key pinnen. Zertifikatswechsel daher &#039;&#039;&#039;rechtzeitig ankündigen&#039;&#039;&#039; und mit einem &#039;&#039;&#039;Backup-Pin&#039;&#039;&#039; vorbereiten - der neue Pin muss vor dem Wechsel in einer Client-Version ausgerollt sein, sonst sperrt sich die App aus.&lt;br /&gt;
==Automatische Zertifikatserneuerung==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server kann sein TLS-Zertifikat selbst bei einer Zertifizierungsstelle beantragen und vor Ablauf erneuern. Prüfarten, Voraussetzungen, Einrichtung, Ablauf und Konsolenbefehle beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]].&lt;br /&gt;
&lt;br /&gt;
===Felder===&lt;br /&gt;
&lt;br /&gt;
Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                       !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Automatische Erneuerung    || &#039;&#039;deaktiviert&#039;&#039; = aus. &#039;&#039;Produktivbetrieb&#039;&#039; = der Dienst beantragt und erneuert das Zertifikat selbst. &#039;&#039;Testbetrieb&#039;&#039; = nur für Testsysteme, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung#Testbetrieb|Testbetrieb]]&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikatsname (FQDN)     || Der Name, auf den das Zertifikat ausgestellt wird. Pflicht, sobald die Erneuerung aktiv ist&lt;br /&gt;
|-&lt;br /&gt;
| Kontakt bei der CA         || Optional. Die Zertifizierungsstelle schickt an diese Adresse Warnungen, wenn ein Zertifikat abzulaufen droht&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikat gültig bis      || Ablaufdatum des aktiven Zertifikats&lt;br /&gt;
|-&lt;br /&gt;
| Testzertifikat aus Staging || Gesetzt, wenn das aktive Zertifikat aus dem Testbetrieb stammt&lt;br /&gt;
|-&lt;br /&gt;
| Zustand                    || &#039;&#039;idle&#039;&#039; = nichts zu tun, &#039;&#039;pending&#039;&#039; = Vorgang läuft, &#039;&#039;error&#039;&#039; = letzter Versuch fehlgeschlagen&lt;br /&gt;
|-&lt;br /&gt;
| Letzte Meldung             || Ergebnis des letzten Vorgangs - im Störungsfall das erste Feld, in das Sie sehen&lt;br /&gt;
|-&lt;br /&gt;
| Gesperrt bis               || Nach einem Fehlschlag wartet der Dienst bis zu diesem Zeitpunkt, bevor er es erneut versucht&lt;br /&gt;
|-&lt;br /&gt;
| Letzter Selbsttest am      || Zeitpunkt der letzten Prüfung über das Testsystem der Zertifizierungsstelle&lt;br /&gt;
|-&lt;br /&gt;
| Selbsttest bestanden       || Ergebnis dieser Prüfung&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die Felder ab &#039;&#039;Zertifikat gültig bis&#039;&#039; füllt der Dienst selbst; sie dienen der Anzeige.&lt;br /&gt;
&lt;br /&gt;
Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                       !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Prüfart                    || &#039;&#039;http-01 (Port 80)&#039;&#039; oder &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039;, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung#Welche Prüfart?|Welche Prüfart?]]&lt;br /&gt;
|-&lt;br /&gt;
| DNS-Anbieter               || Nur dns-01: &#039;&#039;acme-dns&#039;&#039;, &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Adresse (acme-dns)         || Nur acme-dns: Adresse der Instanz, beginnt mit &amp;lt;code&amp;gt;https://&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Benutzer (acme-dns)        || Nur acme-dns: Benutzer des Kontos. Bleiben Benutzer, Schlüssel und Name leer, legt der Dienst das Konto selbst an&lt;br /&gt;
|-&lt;br /&gt;
| Schlüssel / API-Token      || acme-dns: Passwort des Kontos. Hetzner und Cloudflare: API-Token. IONOS: API-Schlüssel in der Form &amp;lt;code&amp;gt;Präfix.Secret&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Name des TXT-Eintrags      || Leer = &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Zertifikatsname&amp;gt;&amp;lt;/code&amp;gt;. Bei CNAME-Delegation das Ziel des CNAME, bei acme-dns die &#039;&#039;fulldomain&#039;&#039; des Kontos&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Die Zugangsdaten stehen im Klartext im Server-Profil. Verwenden Sie Token, die nur die nötige Zone ändern dürfen.}}&lt;br /&gt;
&lt;br /&gt;
==Rate-Limit je Profil==&lt;br /&gt;
&lt;br /&gt;
Jedes Server-Profil führt &#039;&#039;&#039;eigene Zähler&#039;&#039;&#039; und kann &#039;&#039;&#039;eigene Schwellen&#039;&#039;&#039; haben.&lt;br /&gt;
Ein Profil, das gerade überrannt wird, bremst damit die anderen Profile nicht mit&lt;br /&gt;
aus - das Public-Profil und das interne mTLS-Profil sind voneinander unabhängig.&lt;br /&gt;
&lt;br /&gt;
Gezählt wird pro Schlüssel, und der Schlüssel ist der &#039;&#039;&#039;API-Key&#039;&#039;&#039; des Zugangs.&lt;br /&gt;
Sendet ein Konsument keinen API-Key (Zugang mit &#039;&#039;*&#039;&#039;), wird ersatzweise auf die&lt;br /&gt;
&#039;&#039;&#039;IP-Adresse&#039;&#039;&#039; gezählt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Das ist der wichtigste Punkt bei mobilen Anwendungen: Sitzen alle&lt;br /&gt;
Benutzer hinter einer gemeinsamen Firmen-IP und sendet die App keinen eigenen&lt;br /&gt;
API-Key, teilen sich &#039;&#039;&#039;alle Benutzer einen Zähler&#039;&#039;&#039;. Mit den Standardwerten sind&lt;br /&gt;
das 10 Fehlversuche für den ganzen Betrieb pro Minute - nach ein paar falschen&lt;br /&gt;
Passworteingaben steht die Abteilung. Für solche Profile die Schwellen bewusst&lt;br /&gt;
höher setzen oder jedem Konsumenten einen eigenen API-Key geben.}}&lt;br /&gt;
&lt;br /&gt;
Dasselbe gilt hinter einem Reverse-Proxy: Alle Anfragen kommen dort von der IP-Adresse&lt;br /&gt;
des Proxys, und alle Konsumenten ohne API-Key teilen sich einen Zähler. Der Header&lt;br /&gt;
&#039;&#039;X-Forwarded-For&#039;&#039; ändert daran nichts, er wird nur protokolliert.&lt;br /&gt;
&lt;br /&gt;
Bleiben alle vier Felder leer (bzw. 0), gelten die Standardwerte. Die Werte werden&lt;br /&gt;
&#039;&#039;&#039;beim Start&#039;&#039;&#039; des REST-Dienstes gelesen - eine Änderung wird erst nach einem&lt;br /&gt;
Neustart wirksam.&lt;br /&gt;
&lt;br /&gt;
==Validierung beim Speichern==&lt;br /&gt;
&lt;br /&gt;
Immer geprüft wird:&lt;br /&gt;
&lt;br /&gt;
* Nr und Name sind vergeben, Nr liegt zwischen 1 und 99.&lt;br /&gt;
* Nr und Name sind eindeutig.&lt;br /&gt;
* Eingetragene Zertifikate und Schlüssel liegen im PEM-Format vor.&lt;br /&gt;
* Ein eingetragener Zertifikatsname enthält keine Adresse, keinen Port und keine Leerzeichen.&lt;br /&gt;
&lt;br /&gt;
Nur bei &#039;&#039;&#039;aktivem&#039;&#039;&#039; Profil zusätzlich:&lt;br /&gt;
&lt;br /&gt;
* Ohne &#039;&#039;kein SSL&#039;&#039; und ohne automatische Erneuerung sind Zertifikat und Schlüssel vorhanden.&lt;br /&gt;
* Bei &#039;&#039;mTLS-Modus aktiv&#039;&#039; ist das Root-Zertifikat vorhanden.&lt;br /&gt;
* Bei aktiver Erneuerung ist der Zertifikatsname vergeben, und &#039;&#039;kein SSL&#039;&#039; ist nicht gesetzt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein &#039;&#039;&#039;nicht aktives&#039;&#039;&#039; Profil dürfen Sie unvollständig speichern - es wird beim Start des Dienstes nicht geladen. Was Sie eintragen, wird trotzdem auf Plausibilität geprüft.}}&lt;br /&gt;
&lt;br /&gt;
==Bindungen==&lt;br /&gt;
&lt;br /&gt;
Eine Bindung ist die Kombination aus IP-Adresse und Port, auf der ein Server-Profil lauscht. Pro Server-Profil können mehrere Bindungen existieren, z.B. um die gleiche TLS-Konfiguration parallel auf 443 und 8443 anzubieten.&lt;br /&gt;
&lt;br /&gt;
===Aufruf===&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste den gewünschten Server markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die zum Server gehörende Bindungs-Liste.&lt;br /&gt;
&lt;br /&gt;
===Felder===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld    !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Host    || IP-Adresse oder Hostname, auf dem gelauscht wird. &#039;&#039;0.0.0.0&#039;&#039; = alle IPv4-Adressen; &#039;&#039;127.0.0.1&#039;&#039; = nur lokal; explizite IPs binden gezielt auf eine Netzwerkkarte&lt;br /&gt;
|-&lt;br /&gt;
| Port    || TCP-Port (1-65535). 443/8443 für HTTPS, frei wählbar für Debug-Profile&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv   || Bindung wird beim Server-Start geladen&lt;br /&gt;
|-&lt;br /&gt;
| Standard|| Markiert die Standard-Bindung des Servers. Pro Server-Profil ist nur eine Standard-Bindung erlaubt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Wirksamkeit===&lt;br /&gt;
&lt;br /&gt;
Bindungen werden ausschliesslich beim Start des REST-Dienstes geladen. Änderungen werden daher erst nach einem Neustart aktiv. Bis dahin laufen die alten Bindungen weiter.&lt;br /&gt;
&lt;br /&gt;
==Listen-Funktionen==&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfg&#039;&#039;&#039; - neues Server-Profil&lt;br /&gt;
* &#039;&#039;&#039;Return&#039;&#039;&#039; - vorhandenes Profil bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F4&#039;&#039;&#039; - Sortierung wählen&lt;br /&gt;
* &#039;&#039;&#039;F6&#039;&#039;&#039; - Bindungs-Liste des markierten Servers&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/%C3%96ffentliche_Freigabe&amp;diff=67077</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/%C3%96ffentliche_Freigabe&amp;diff=67077"/>
		<updated>2026-10-26T10:21:57Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Zertifikat in allen Varianten nötig, auch hinter Proxy und im VPN [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
{{Seitentyp|Übersicht}}&lt;br /&gt;
= Öffentliche Freigabe =&lt;br /&gt;
&lt;br /&gt;
Apps auf mobilen Geräten und externe Systeme erreichen den REST-Server nur, wenn er aus dem Internet erreichbar ist. Diese Seite stellt die drei Wege dafür gegenüber und richtet sich an die IT-Administration, die über die Freigabe entscheidet.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der REST-Dienst läuft auf dem OBS-Server oder auf einem [[#REST-Dienst auf einem eigenen Server|eigenen Server]]. Auf dem OBS-Server läuft häufig auch die Datenbank. Die Varianten unterscheiden sich vor allem darin, ob eine Verbindung aus dem Internet den Server des REST-Dienstes direkt erreicht. Ein Zertifikat ist in allen Varianten nötig, auch im VPN, weil die App nur per HTTPS verbindet.}}&lt;br /&gt;
&lt;br /&gt;
== Varianten im Vergleich ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Merkmal !! Direkte Portfreigabe !! Reverse-Proxy !! VPN&lt;br /&gt;
|-&lt;br /&gt;
| Aus dem Internet erreichbar || Server des REST-Dienstes: Port des REST-Servers, bei Prüfart http-01 zusätzlich Port 80 || nur der Proxy (Port 443) || nur das VPN-Gateway&lt;br /&gt;
|-&lt;br /&gt;
| Aufwand für die Nutzer der App || keiner || keiner || VPN-Client einrichten und vor der Nutzung einschalten&lt;br /&gt;
|-&lt;br /&gt;
| Aufwand für die IT || Portweiterleitung am Router || Proxy betreiben, Zertifikat am Proxy || VPN-Zugang für jedes Gerät&lt;br /&gt;
|-&lt;br /&gt;
| Filtern und Protokollieren vor dem REST-Dienst || – || ✓ || nur auf Netzebene&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikat || im REST-Server, Prüfart http-01 oder dns-01 || öffentlich am Proxy, dazu eines im REST-Server für die Strecke vom Proxy || im REST-Server, Prüfart dns-01&lt;br /&gt;
|-&lt;br /&gt;
| REST-Server sieht die IP-Adresse des Geräts || ✓ || nur im Protokoll (über &#039;&#039;X-Forwarded-For&#039;&#039;) || ✓, sofern das VPN-Gateway die Adressen nicht umsetzt (NAT)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Schutz durch den REST-Server ==&lt;br /&gt;
Unabhängig von der Variante schützt sich der REST-Server selbst. Diese Mechanismen greifen in allen drei Varianten:&lt;br /&gt;
* &#039;&#039;&#039;Verschlüsselung:&#039;&#039;&#039; ausschliesslich TLS 1.2 mit Forward Secrecy und AEAD-Verfahren. Unverschlüsselte Anfragen an einen TLS-Port werden vor jeder Verarbeitung abgewiesen. Optional mTLS: jeder Client braucht ein gültiges Client-Zertifikat.&lt;br /&gt;
* &#039;&#039;&#039;Zugänge:&#039;&#039;&#039; Jede Anfrage braucht einen gültigen API-Key, wahlweise zusätzlich JWT. Jeder Zugang ist nur für die Endpunkte freigeschaltet, die er braucht, und lässt sich auf eine Absenderadresse beschränken. API-Keys lassen sich ohne Unterbrechung tauschen.&lt;br /&gt;
* &#039;&#039;&#039;Widerrufbare Sitzungen:&#039;&#039;&#039; Eine Abmeldung wirkt sofort. Ein Refresh-Token gilt genau einmal; wird ein verbrauchtes erneut vorgelegt, sperrt der Server die ganze Sitzung.&lt;br /&gt;
* &#039;&#039;&#039;Grenzen gegen Missbrauch:&#039;&#039;&#039; getrenntes Rate-Limit für erfolgreiche und fehlgeschlagene Anfragen, eine Obergrenze gleichzeitiger Anfragen und Verbindungen sowie Grössengrenzen für Anfragen und Uploads.&lt;br /&gt;
* &#039;&#039;&#039;Keine Informationen nach aussen:&#039;&#039;&#039; Fehlerantworten enthalten nie interne Details. Die stehen nur im Protokoll, mit einer Korrelations-ID je Anfrage. Kennwörter und Token werden dort maskiert.&lt;br /&gt;
* &#039;&#039;&#039;Kurze Öffnung für die Zertifikatsprüfung:&#039;&#039;&#039; Bei http-01 öffnet der Dienst Port 80 nur für die Dauer der Prüfung.&lt;br /&gt;
&lt;br /&gt;
Diese Mechanismen schützen den REST-Dienst. Reverse-Proxy und VPN fügen eine Schicht davor hinzu: Sie halten den Server des REST-Dienstes aus dem Internet heraus, und mit ihm häufig auch den OBS-Server und die Datenbank. Das wirkt auch gegen Schwachstellen ausserhalb des REST-Servers und gegen Überlastung.&lt;br /&gt;
Der Router leitet den Port des REST-Servers aus dem Internet auf den Server des REST-Dienstes weiter. Für die Zertifikatserneuerung mit der Prüfart http-01 wird zusätzlich Port 80 weitergeleitet.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Die einfachste Einrichtung, es wird keine weitere Komponente gebraucht.&lt;br /&gt;
* Die App funktioniert überall, ohne dass die Nutzer etwas tun müssen.&lt;br /&gt;
* Der REST-Server sieht die IP-Adresse jedes Geräts. Alle Schutzmechanismen des REST-Servers, auch Host-Beschränkung und Rate-Limit, arbeiten damit je Gerät.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Der Server des REST-Dienstes ist direkt aus dem Internet erreichbar. Läuft der REST-Dienst auf dem OBS-Server, gilt das für den OBS-Server und häufig auch für die Datenbank. Die [[#Schutz durch den REST-Server|Schutzmechanismen des REST-Servers]] greifen, eine zusätzliche Schicht davor gibt es aber nicht.&lt;br /&gt;
* Mit der Prüfart http-01 ist zusätzlich Port 80 weitergeleitet. Mit der Prüfart dns-01 entfällt das.&lt;br /&gt;
&lt;br /&gt;
== Reverse-Proxy ==&lt;br /&gt;
Ein Reverse-Proxy, zum Beispiel in einer DMZ oder als Funktion der Firewall, nimmt HTTPS auf Port 443 an. Er prüft die Anfrage und reicht sie über eine eigene, ebenfalls verschlüsselte Verbindung an den REST-Server weiter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Der Server des REST-Dienstes ist aus dem Internet nicht erreichbar, nur der Proxy.&lt;br /&gt;
* Am Proxy lassen sich Anfragen nach Ihren Vorgaben filtern, drosseln und protokollieren.&lt;br /&gt;
* Für die Nutzer der App ändert sich nichts.&lt;br /&gt;
* Das öffentliche Zertifikat liegt am Proxy. Für die Strecke vom Proxy genügt im REST-Server ein Zertifikat Ihrer internen Zertifizierungsstelle oder ein selbst signiertes. Es muss vor Ablauf getauscht werden.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Der Proxy muss betrieben und gepflegt werden.&lt;br /&gt;
* Der REST-Server sieht als Absender nur den Proxy. Die Host-Beschränkung eines Zugangs kann keine Geräte unterscheiden, und Anfragen ohne API-Key teilen sich einen Zähler des Rate-Limits. Die IP-Adresse des Geräts steht nur im Protokoll, wenn der Proxy den Header &#039;&#039;X-Forwarded-For&#039;&#039; setzt.&lt;br /&gt;
&lt;br /&gt;
Die Einrichtung beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung#Betrieb hinter einem Reverse-Proxy|Betrieb hinter einem Reverse-Proxy]].&lt;br /&gt;
&lt;br /&gt;
== VPN ==&lt;br /&gt;
Die Geräte verbinden sich per VPN mit dem Netz, in dem der REST-Dienst läuft. Die App spricht durch den Tunnel per HTTPS mit dem REST-Server. Aus dem Internet ist nur das VPN-Gateway erreichbar.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Für den REST-Server ist kein Port aus dem Internet offen.&lt;br /&gt;
* Nur Geräte mit VPN-Zugang erreichen den REST-Server.&lt;br /&gt;
* Setzt das VPN-Gateway die Adressen nicht um, sieht der REST-Server die VPN-Adresse jedes Geräts.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Jedes Gerät braucht einen VPN-Client und einen eigenen Zugang. Ohne zentrale Geräteverwaltung schalten die Nutzer das VPN von Hand ein. Ist es aus, meldet die App, dass der Server nicht erreichbar ist.&lt;br /&gt;
* Ein VPN-Zugang öffnet den Weg ins Netz. Beschränken Sie ihn auf die Adresse und den Port des REST-Servers.&lt;br /&gt;
* Das Zertifikat lässt sich nur mit der Prüfart dns-01 erneuern, weil die Zertifizierungsstelle den Server nicht erreicht.&lt;br /&gt;
* Der Name des REST-Servers muss auf dem Gerät auf die interne Adresse zeigen. Das geht über einen DNS-Server, den das VPN mitliefert, oder über einen öffentlichen DNS-Eintrag mit der internen Adresse. Manche Router verwerfen solche Antworten als Schutz gegen DNS-Rebinding; dann löst der Name in diesem Netz nicht auf.&lt;br /&gt;
&lt;br /&gt;
Das Zertifikat richten Sie wie unter [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]] beschrieben mit der Prüfart dns-01 ein.&lt;br /&gt;
&lt;br /&gt;
== REST-Dienst auf einem eigenen Server ==&lt;br /&gt;
Der REST-Dienst muss nicht auf dem OBS-Server laufen. Er lässt sich auch auf einem eigenen Server betreiben, zum Beispiel in einer DMZ. Voraussetzung ist, dass dieser Server&lt;br /&gt;
* per SMB auf den OBS-Server zugreifen kann und&lt;br /&gt;
* die Datenbank erreicht.&lt;br /&gt;
&lt;br /&gt;
Der eigene Server lässt sich mit jeder der drei Varianten kombinieren. Bei der direkten Portfreigabe zeigt die Weiterleitung dann auf diesen Server statt auf den OBS-Server.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Eine Verbindung aus dem Internet erreicht weder den OBS-Server noch die Datenbank direkt, auch bei direkter Portfreigabe.&lt;br /&gt;
* Die Verarbeitung der Anfragen läuft nicht auf dem OBS-Server. Die Datenbank wird weiterhin belastet.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Ein weiterer Server muss betrieben und gepflegt werden.&lt;br /&gt;
* Vom eigenen Server ins interne Netz müssen SMB und der Port der Datenbank offen sein. Beschränken Sie diese Wege auf den OBS-Server und den Datenbankserver.&lt;br /&gt;
&lt;br /&gt;
== Entscheidungshilfe ==&lt;br /&gt;
* &#039;&#039;&#039;Keine eingehende Verbindung zum OBS-Server erlaubt, DMZ oder Firewall mit Proxy-Funktion vorhanden:&#039;&#039;&#039; Reverse-Proxy.&lt;br /&gt;
* &#039;&#039;&#039;Keine eingehende Verbindung zum OBS-Server erlaubt, aber kein Proxy vorhanden:&#039;&#039;&#039; REST-Dienst auf einem eigenen Server in einer DMZ, mit direkter Portfreigabe auf diesen Server.&lt;br /&gt;
* &#039;&#039;&#039;Geräte werden zentral verwaltet oder ein VPN ist ohnehin im Einsatz:&#039;&#039;&#039; VPN.&lt;br /&gt;
* &#039;&#039;&#039;Kleine Installation ohne Vorgaben der IT:&#039;&#039;&#039; Direkte Portfreigabe, möglichst mit der Prüfart dns-01, damit Port 80 geschlossen bleibt.&lt;br /&gt;
&lt;br /&gt;
== Siehe auch ==&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge#Host-Beschränkung|Host-Beschränkung]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zertifikatserneuerung&amp;diff=67074</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zertifikatserneuerung&amp;diff=67074"/>
		<updated>2026-10-26T10:12:38Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: http-01: Server des REST-Dienstes statt OBS-Server [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
{{Seitentyp|Anleitung}}&lt;br /&gt;
= Zertifikatserneuerung =&lt;br /&gt;
&lt;br /&gt;
Der REST-Server kann sein TLS-Zertifikat selbst bei der Zertifizierungsstelle Let&#039;s Encrypt beantragen und vor Ablauf erneuern. Zertifikat und privater Schlüssel entstehen auf Ihrem Server und verlassen ihn nicht. Diese Seite beschreibt, wie Sie die Erneuerung je nach Netzaufbau einrichten.&lt;br /&gt;
&lt;br /&gt;
{{Auf einen Blick|1=&lt;br /&gt;
* &#039;&#039;&#039;Zwei Prüfarten.&#039;&#039;&#039; http-01 prüft über Port 80, dns-01 über einen DNS-Eintrag und braucht keinen offenen Port.&lt;br /&gt;
* &#039;&#039;&#039;Selbsttest vorab.&#039;&#039;&#039; Vor dem ersten Zertifikat und danach wöchentlich prüft der Dienst den Weg über das Testsystem der Zertifizierungsstelle.&lt;br /&gt;
* &#039;&#039;&#039;Erneuerung ab 30 Tagen Restlaufzeit.&#039;&#039;&#039; Das bisherige Zertifikat bleibt als Rückfall erhalten.&lt;br /&gt;
* &#039;&#039;&#039;dns-01 braucht nur ausgehende Verbindungen&#039;&#039;&#039; und eignet sich damit für VPN und für Server ohne Portfreigabe.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Voraussetzungen ==&lt;br /&gt;
* Ein Name, auf den das Zertifikat lautet, zum Beispiel &#039;&#039;obs.firma.de&#039;&#039;. Platzhalter-Zertifikate (&#039;&#039;*.firma.de&#039;&#039;) werden nicht unterstützt.&lt;br /&gt;
* Ein Server-Profil mit TLS, also ohne Haken &#039;&#039;&#039;kein SSL&#039;&#039;&#039;.&lt;br /&gt;
* Ausgehende HTTPS-Verbindungen zur Zertifizierungsstelle (&amp;lt;code&amp;gt;acme-v02.api.letsencrypt.org&amp;lt;/code&amp;gt; und für den Selbsttest &amp;lt;code&amp;gt;acme-staging-v02.api.letsencrypt.org&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Bei dns-01 zusätzlich ausgehende HTTPS-Verbindungen zur Schnittstelle des DNS-Anbieters bzw. zur acme-dns-Instanz.&lt;br /&gt;
&lt;br /&gt;
=== Welche Prüfart? ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Prüfart !! Funktioniert, wenn !! Aus dem Internet erreichbar sein muss&lt;br /&gt;
|-&lt;br /&gt;
| http-01 || der Name öffentlich auf diesen Server zeigt und Port 80 von aussen auf ihn weitergeleitet ist || Port 80&lt;br /&gt;
|-&lt;br /&gt;
| dns-01 || sich der DNS-Eintrag &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; über eine Schnittstelle schreiben lässt, direkt oder über einen CNAME || nichts&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Welche Prüfart zu Ihrer [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|öffentlichen Freigabe]] passt:&lt;br /&gt;
* &#039;&#039;&#039;Direkte Portfreigabe:&#039;&#039;&#039; http-01 oder dns-01. Mit dns-01 bleibt Port 80 geschlossen.&lt;br /&gt;
* &#039;&#039;&#039;Reverse-Proxy:&#039;&#039;&#039; Das öffentliche Zertifikat liegt am Proxy, die Erneuerung im REST-Server wird nicht gebraucht.&lt;br /&gt;
* &#039;&#039;&#039;VPN:&#039;&#039;&#039; dns-01.&lt;br /&gt;
&lt;br /&gt;
=== Wege bei dns-01 ===&lt;br /&gt;
Bei dns-01 fragt die Zertifizierungsstelle den TXT-Eintrag &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; ab. Der Dienst schreibt ihn vor der Prüfung und löscht ihn danach wieder. Dafür gibt es drei Wege:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Weg !! Wann !! Einmalig beim DNS des Namens !! Einstellung &#039;&#039;&#039;DNS-Anbieter&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Direkt beim Anbieter || Die Zone des Namens liegt bei Hetzner, Cloudflare oder IONOS || nichts, Sie brauchen nur ein API-Token || &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| CNAME auf eine Zone bei einem Anbieter || Die Zone des Namens liegt bei einem anderen Anbieter, oder im REST-Server sollen keine Zugangsdaten zur eigenen Zone liegen || CNAME &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf einen Namen in einer Zone bei Hetzner, Cloudflare oder IONOS || &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| CNAME auf acme-dns || Eine acme-dns-Instanz steht zur Verfügung, im eigenen Betrieb oder bei einem Dienstleister || CNAME &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf die &#039;&#039;fulldomain&#039;&#039; des acme-dns-Kontos || &#039;&#039;acme-dns&#039;&#039;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim CNAME-Weg genügt beim DNS-Anbieter des Namens ein von Hand angelegter Eintrag, auch bei Anbietern ohne Schnittstelle. Der REST-Server bekommt dann nur Zugang zur Zone, auf die der CNAME zeigt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Namen von DynDNS-Diensten (zum Beispiel &#039;&#039;…myfritz.net&#039;&#039;) eignen sich in der Regel nicht als Zertifikatsname, weil sich dort keine eigenen Einträge anlegen lassen. Verwenden Sie einen Namen unter Ihrer eigenen Domain. Der DynDNS-Name bleibt davon unberührt, zum Beispiel für den Zugang zum VPN.}}&lt;br /&gt;
&lt;br /&gt;
== Vorgehen ==&lt;br /&gt;
=== Gemeinsame Einstellungen ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Wählen Sie &#039;&#039;&#039;Stammdaten → Z Weitere Stammdaten → REST-Server → Server&#039;&#039;&#039; und öffnen Sie das Server-Profil.&lt;br /&gt;
# Stellen Sie auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039;.&lt;br /&gt;
# Tragen Sie im Feld &#039;&#039;&#039;Zertifikatsname (FQDN)&#039;&#039;&#039; den Namen ein, auf den das Zertifikat lauten soll.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Kontakt bei der CA&#039;&#039;&#039; eine E-Mail-Adresse ein. Die Zertifizierungsstelle schickt dorthin Warnungen, wenn ein Zertifikat abzulaufen droht.&lt;br /&gt;
# Richten Sie die Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; nach einem der folgenden Abschnitte ein und speichern Sie.&lt;br /&gt;
# Starten Sie den REST-Dienst neu.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart http-01 ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Lassen Sie den Namen öffentlich auf die Adresse Ihres Internetanschlusses zeigen.&lt;br /&gt;
# Leiten Sie am Router Port 80 von aussen auf den Server des REST-Dienstes weiter.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; im Feld &#039;&#039;&#039;Prüfart&#039;&#039;&#039; den Eintrag &#039;&#039;http-01 (Port 80)&#039;&#039;. Die übrigen Felder bleiben leer.&lt;br /&gt;
# Geben Sie in der Konsole des REST-Servers &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Port 80 darf auf dem Server des REST-Dienstes von keinem anderen Dienst belegt sein, häufig ist das ein Webserver wie IIS. Der REST-Server öffnet den Port nur für die Dauer der Prüfung und schliesst ihn danach wieder.&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 direkt beim DNS-Anbieter ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Erstellen Sie beim Anbieter der Zone ein API-Token:&lt;br /&gt;
#* &#039;&#039;&#039;Hetzner:&#039;&#039;&#039; ein API-Token des Projekts mit Lese- und Schreibrecht in der Hetzner Console. Zonen in der früheren DNS-Konsole (dns.hetzner.com) werden nicht unterstützt.&lt;br /&gt;
#* &#039;&#039;&#039;Cloudflare:&#039;&#039;&#039; ein API-Token mit der Berechtigung, DNS zu bearbeiten, beschränkt auf die Zone.&lt;br /&gt;
#* &#039;&#039;&#039;IONOS:&#039;&#039;&#039; einen API-Schlüssel. Er besteht aus Präfix und Secret.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; im Feld &#039;&#039;&#039;Prüfart&#039;&#039;&#039; den Eintrag &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und im Feld &#039;&#039;&#039;DNS-Anbieter&#039;&#039;&#039; den Anbieter.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; das Token ein, bei IONOS in der Form &amp;lt;code&amp;gt;Präfix.Secret&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Lassen Sie &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; leer.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 mit CNAME auf eine Zone bei einem DNS-Anbieter ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Legen Sie in einer Zone bei Hetzner, Cloudflare oder IONOS einen Namen für die Prüfung fest, zum Beispiel &#039;&#039;obs-firma.acme.example.de&#039;&#039;. Einen Eintrag müssen Sie dort nicht anlegen.&lt;br /&gt;
# Legen Sie beim DNS-Anbieter Ihres Namens einen CNAME an: &amp;lt;code&amp;gt;_acme-challenge.obs.firma.de&amp;lt;/code&amp;gt; auf &#039;&#039;obs-firma.acme.example.de&#039;&#039;.&lt;br /&gt;
# Erstellen Sie ein API-Token für die Zone, in der das Ziel liegt, wie im vorigen Abschnitt beschrieben.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; die Prüfart &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und den Anbieter dieser Zone.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; das Token ein.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; das Ziel des CNAME ein, hier &#039;&#039;obs-firma.acme.example.de&#039;&#039;.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 mit CNAME auf acme-dns ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; die Prüfart &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und den DNS-Anbieter &#039;&#039;acme-dns&#039;&#039;.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Adresse (acme-dns)&#039;&#039;&#039; die Adresse der Instanz ein. Sie beginnt mit &amp;lt;code&amp;gt;https://&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Lassen Sie &#039;&#039;&#039;Benutzer (acme-dns)&#039;&#039;&#039;, &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; und &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; leer und speichern Sie.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein. Der Dienst legt das Konto bei acme-dns an und trägt Benutzer, Schlüssel und Name selbst ein. Unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; steht danach der CNAME, den Sie anlegen müssen.&lt;br /&gt;
# Legen Sie beim DNS-Anbieter Ihres Namens diesen CNAME an: &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf die genannte &#039;&#039;fulldomain&#039;&#039;.&lt;br /&gt;
# Geben Sie in der Konsole erneut &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Leeren Sie die drei Felder später nicht. Der Dienst würde ein neues Konto mit einer neuen &#039;&#039;fulldomain&#039;&#039; anlegen, und der gesetzte CNAME zeigte ins Leere. Ein bestehendes Konto tragen Sie mit allen drei Angaben ein. Teilweise gefüllte Angaben weist die Maske beim Speichern ab.}}&lt;br /&gt;
&lt;br /&gt;
=== Erstes Zertifikat holen ===&lt;br /&gt;
Ist der Selbsttest bestanden, holt der Dienst das Zertifikat bei der nächsten stündlichen Prüfung. Sofort geht es mit &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; in der Konsole. Bis das erste Zertifikat da ist, kann sich kein Client mit dem Server-Profil verbinden.&lt;br /&gt;
&lt;br /&gt;
=== Testbetrieb ===&lt;br /&gt;
{{Achtung|Im &#039;&#039;&#039;Testbetrieb&#039;&#039;&#039; beantragt der Dienst das Zertifikat beim Testsystem der Zertifizierungsstelle. Einem solchen Zertifikat vertraut &#039;&#039;&#039;kein&#039;&#039;&#039; Client, Anwendungen können sich danach nicht mehr mit diesem Server verbinden. Verwenden Sie den Testbetrieb ausschliesslich auf Testsystemen. Zurück in den Produktivbetrieb kommen Sie, indem Sie das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039; stellen. Der Dienst holt daraufhin sofort ein gültiges Zertifikat, auch wenn das Testzertifikat noch lange läuft.}}&lt;br /&gt;
&lt;br /&gt;
== Ergebnis ==&lt;br /&gt;
Nach bestandenem Selbsttest ist auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; der Haken &#039;&#039;&#039;Selbsttest bestanden&#039;&#039;&#039; gesetzt. Unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; steht &amp;quot;Selbsttest bestanden - von aussen erreichbar&amp;quot; (http-01) bzw. &amp;quot;Selbsttest bestanden - DNS-Eintrag von der CA geprueft&amp;quot; (dns-01). Nach der Ausstellung zeigt &#039;&#039;&#039;Zertifikat gültig bis&#039;&#039;&#039; das Ablaufdatum, und &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; in der Konsole zeigt den Stand je Profil.&lt;br /&gt;
&lt;br /&gt;
Danach läuft die Erneuerung selbständig:&lt;br /&gt;
# &#039;&#039;&#039;Prüfung:&#039;&#039;&#039; Der Dienst sieht stündlich nach, ob etwas ansteht.&lt;br /&gt;
# &#039;&#039;&#039;Selbsttest:&#039;&#039;&#039; Einmal wöchentlich und nach jeder Änderung von Zertifikatsname, Prüfart, DNS-Anbieter oder Name des TXT-Eintrags prüft der Dienst den Weg über das Testsystem der Zertifizierungsstelle. Besteht der Test nicht, beantragt er kein Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Erneuerung:&#039;&#039;&#039; Ab 30 Tagen Restlaufzeit beantragt der Dienst ein neues Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Übernahme:&#039;&#039;&#039; Der Dienst prüft das neue Zertifikat, speichert es und bewahrt das bisherige als Rückfall auf. Anschliessend startet nur das betroffene Server-Profil neu.&lt;br /&gt;
# &#039;&#039;&#039;Rückfall:&#039;&#039;&#039; Startet das Profil mit dem neuen Zertifikat nicht, aktiviert der Dienst wieder das vorherige.&lt;br /&gt;
&lt;br /&gt;
Nach einem Fehlschlag wartet der Dienst zunehmend länger, bevor er es erneut versucht: eine, zwei, vier, acht und höchstens 24 Stunden. Das Feld &#039;&#039;&#039;Gesperrt bis&#039;&#039;&#039; zeigt, bis wann. &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; in der Konsole wirken sofort, auch während der Sperre.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; || Zeigt je Profil Betriebsart, Ablaufdatum, Zustand und das Ergebnis des letzten Selbsttests&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; || Prüft sofort den Weg über das Testsystem der Zertifizierungsstelle. Beantragt kein Zertifikat&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; || Stösst die Erneuerung sofort an, auch wenn sie noch nicht fällig ist&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Bei dns-01 wartet der Dienst nach dem Schreiben des Eintrags 30 Sekunden und dann bis zu drei Minuten, bis der Eintrag über öffentliche DNS-Server sichtbar ist. Erst dann löst er die Prüfung aus.&lt;br /&gt;
&lt;br /&gt;
== Wenn es nicht klappt ==&lt;br /&gt;
Die Meldungen stehen unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; und im Protokoll des REST-Servers.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Meldung !! Ursache&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Port 80 laesst sich nicht oeffnen - er ist belegt&amp;quot; || Ein anderer Dienst auf dem Server des REST-Dienstes belegt Port 80, häufig IIS. Dienst abschalten oder die Prüfart dns-01 verwenden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;dns-01 ist eingestellt, aber kein DNS-Anbieter&amp;quot; || Auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; ist kein DNS-Anbieter gewählt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;kein API-Token&amp;quot; || Das Feld &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; ist leer&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;acme-dns: Konto unvollstaendig&amp;quot; || Von Benutzer, Schlüssel und Name des TXT-Eintrags ist nur ein Teil gefüllt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Hetzner: keine Zone zu … im Projekt des Tokens&amp;quot; || Das Token gehört zu einem anderen Projekt, oder die Zone liegt noch in der früheren DNS-Konsole&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Cloudflare: keine Zone zu …, die das Token sehen darf&amp;quot; || Das Token ist nicht für diese Zone freigegeben&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;IONOS: keine Zone zu … im Vertrag des Schluessels&amp;quot; || Der API-Schlüssel gehört zu einem anderen Vertrag&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;… lehnt den TXT-Eintrag ab (HTTP 401)&amp;quot; oder &amp;quot;(HTTP 403)&amp;quot; || Das Token ist ungültig oder darf keine DNS-Einträge ändern&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;… ist nach 210 Sekunden noch nicht ueberall sichtbar&amp;quot; || Der Eintrag ist über öffentliche DNS-Server nicht zu sehen. Meist fehlt der CNAME oder zeigt auf ein anderes Ziel als &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039;. Der Dienst löst die Prüfung trotzdem aus&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;kein oeffentlicher DNS-Server erreichbar&amp;quot; || Ausgehendes DNS (Port 53) ist gesperrt. Der Dienst kann die Sichtbarkeit nicht prüfen und wartet fest 60 Sekunden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;acme-dns-Konto angelegt - jetzt CNAME … setzen&amp;quot; || Kein Fehler: Legen Sie den genannten CNAME an und geben Sie &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Siehe auch ==&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile#Automatische Zertifikatserneuerung|Server-Profile: Felder der Zertifikatserneuerung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=67073</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Einrichtung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=67073"/>
		<updated>2026-10-26T10:12:38Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Reverse-Proxy: Server des REST-Dienstes statt OBS-Server [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Einrichtung eines REST-Endpunktes=&lt;br /&gt;
&lt;br /&gt;
Die folgende Reihenfolge stellt sicher, dass ein lauffähiger Endpunkt mit möglichst wenigen Korrektur-Schleifen entsteht.&lt;br /&gt;
&lt;br /&gt;
==Vorbereitung==&lt;br /&gt;
&lt;br /&gt;
# OBS-Support kontaktieren und das REST-Server-Modul aktivieren lassen.&lt;br /&gt;
# Prüfen, ob auf dem Zielrechner (OBS-Server oder dedizierter REST-Server) Port und IP-Adresse frei sind.&lt;br /&gt;
# Bei TLS-Profilen: TLS-Zertifikat (PEM, mit Kette), privaten Schlüssel (PEM) und - falls mTLS - das CA-Root-Zertifikat bereithalten. Alternativ nutzen Sie die [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|automatische Zertifikatserneuerung]]. Wie der Server von aussen erreichbar wird und welche Variante sich eignet, beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 1: Server-Profil anlegen==&lt;br /&gt;
&lt;br /&gt;
In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server&#039;&#039;&#039; den Punkt &#039;&#039;&#039;Server&#039;&#039;&#039; öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; ein neues Profil anlegen. Pflichtfelder:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Nr&#039;&#039;&#039; - eindeutige Nummer (1-99), wird beim Anlegen automatisch vorgeschlagen.&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - sprechender Name (z.B. &#039;&#039;Public-API&#039;&#039;, &#039;&#039;Internal-mTLS&#039;&#039;, &#039;&#039;Debug-Local&#039;&#039;).&lt;br /&gt;
* Auswahl des Modus:&lt;br /&gt;
** &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039; (Default): Zertifikat und Schlüssel hinterlegen - oder auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; die automatische Erneuerung aktivieren.&lt;br /&gt;
** &#039;&#039;&#039;mTLS-Modus aktiv&#039;&#039;&#039; setzen: zusätzlich das CA-Root-Zertifikat hinterlegen, gegen das die Client-Zertifikate geprüft werden. Jeder Client muss dann ein gültiges Zertifikat vorlegen.&lt;br /&gt;
** &#039;&#039;&#039;kein SSL&#039;&#039;&#039; setzen: ausschliesslich für lokale Tests, der Server akzeptiert dann nur Plain-HTTP.&lt;br /&gt;
&lt;br /&gt;
Optional lassen sich am Profil eigene &#039;&#039;&#039;Rate-Limit-Schwellen&#039;&#039;&#039; hinterlegen. Ohne&lt;br /&gt;
Angabe gelten die Standardwerte (60-Sekunden-Fenster, 120 erfolgreiche und 10&lt;br /&gt;
fehlgeschlagene Anfragen je Schlüssel, 600 Anfragen je Profil). Für Anwendungen&lt;br /&gt;
ohne eigenen API-Key - typisch mobile Apps - sollten die Werte bewusst gesetzt&lt;br /&gt;
werden, weil dann alle Benutzer hinter einer IP auf denselben Zähler laufen.&lt;br /&gt;
&lt;br /&gt;
Details siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 2: Bindung anlegen==&lt;br /&gt;
&lt;br /&gt;
Im Server-Profil mit &#039;&#039;&#039;F6&#039;&#039;&#039; die Bindungs-Liste öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; eine neue Bindung anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - IP-Adresse, auf der gelauscht werden soll (z.B. &#039;&#039;0.0.0.0&#039;&#039; für alle Adressen, &#039;&#039;127.0.0.1&#039;&#039; für lokal).&lt;br /&gt;
* &#039;&#039;&#039;Port&#039;&#039;&#039; - Port-Nummer (typisch 443 für HTTPS, 8099 für Debug, ggf. abweichend). {{Achtung|Port 8080 wird für RustDesk verwendet und darf daher nicht verwendet werden}}&lt;br /&gt;
* &#039;&#039;&#039;Standard&#039;&#039;&#039; setzen, wenn diese Bindung die Standard-Bindung des Servers ist (pro Server-Profil nur eine).&lt;br /&gt;
&lt;br /&gt;
==Schritt 3: REST-Dienst starten / neustarten==&lt;br /&gt;
&lt;br /&gt;
Damit Änderungen an Server-Profilen oder Bindungen wirksam werden, muss der OBS REST-Server-Dienst (oder die Konsole) gestartet bzw. neugestartet werden. Das Profil wird beim Start aus der Datenbank gelesen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 4: Zugang anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Zugänge&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Zugang anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - erscheint im Protokoll und in der Statistik.&lt;br /&gt;
* &#039;&#039;&#039;API-Key&#039;&#039;&#039; - eindeutig, mit dem Schlüsselsymbol kann ein zufälliger Key generiert werden.&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - optional die IP oder der Hostname des Konsumenten (sollte, wenn möglich, immer gesetzt werden).&lt;br /&gt;
* Optional: &#039;&#039;&#039;CORS-Origins&#039;&#039;&#039; und &#039;&#039;&#039;JWT-Konfiguration&#039;&#039;&#039;, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 5: Endpunkt anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Endpunkte&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Endpunkt anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Endpunkt&#039;&#039;&#039; - Ressourcen-/Anzeigename (z.B. &#039;&#039;orders&#039;&#039;, &#039;&#039;kalender&#039;&#039;); dient nur der Übersicht, nicht dem Routing.&lt;br /&gt;
* &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; - der maßgebliche Routing-Pfad, ggf. mit Platzhaltern (z.B. &#039;&#039;/orders&#039;&#039;, &#039;&#039;/orders/{uid}&#039;&#039;, &#039;&#039;/orders/{uid}/modules/{code}&#039;&#039;).&lt;br /&gt;
* &#039;&#039;&#039;Server&#039;&#039;&#039; - die Server-Auswahl bestimmt, über welches Server-Profil der Endpunkt erreichbar ist.&lt;br /&gt;
* &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; - nur setzen, wenn der Endpunkt Dateien entgegennehmen soll; dann auch die &#039;&#039;&#039;Max. Upload-Grösse&#039;&#039;&#039; in MB festlegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6]]).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Eine Versionskennung wird als statisches Segment ins Pfad-Template aufgenommen (z.B. &#039;&#039;/orders/v1&#039;&#039;); ein eigenes Versionsfeld gibt es nicht. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].}}&lt;br /&gt;
&lt;br /&gt;
Anschliessend mit &#039;&#039;&#039;F7&#039;&#039;&#039; das Endpunkt-Skript pflegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]]).&lt;br /&gt;
&lt;br /&gt;
==Schritt 6: Zugang für den Endpunkt freischalten==&lt;br /&gt;
&lt;br /&gt;
Aus der Endpunkt-Liste den neuen Endpunkt markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die Berechtigungs-Liste. Mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; kann ein oder mehrere Zugänge ausgewählt und mit &#039;&#039;&#039;F2&#039;&#039;&#039; übernommen werden. Erst nach dieser Freischaltung darf ein Zugang den Endpunkt aufrufen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 7: Funktionsprüfung==&lt;br /&gt;
&lt;br /&gt;
Aufruf mit einem HTTP-Client (curl, Postman, Browser-Plug-in):&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; https://api.meinserver.de/[Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Bei korrektem Setup erscheint die JSON-Antwort des Skripts. Fehlerfälle werden in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; protokolliert.&lt;br /&gt;
&lt;br /&gt;
==Schritt 8: Statistik und Protokoll prüfen==&lt;br /&gt;
&lt;br /&gt;
* In der Endpunkt-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Endpunkts.&lt;br /&gt;
* In der Zugänge-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Zugangs.&lt;br /&gt;
* In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Protokoll&#039;&#039;&#039; die Ereignisse prüfen.&lt;br /&gt;
&lt;br /&gt;
==Betrieb hinter einem Reverse-Proxy==&lt;br /&gt;
&lt;br /&gt;
Soll der Server des REST-Dienstes nicht direkt aus dem Internet erreichbar sein, nimmt ein Reverse-Proxy die Anfragen entgegen, zum Beispiel in einer DMZ oder als Funktion der Firewall, und reicht sie an den REST-Dienst weiter. Von aussen ist nur der Proxy erreichbar. Vor- und Nachteile gegenüber Portfreigabe und VPN beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]].&lt;br /&gt;
&lt;br /&gt;
Am Proxy und im Netz richten Sie ein:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Öffentlicher Name:&#039;&#039;&#039; zeigt auf die öffentliche IP-Adresse des Proxys.&lt;br /&gt;
* &#039;&#039;&#039;Zertifikat:&#039;&#039;&#039; Der Proxy braucht ein öffentlich gültiges Zertifikat für diesen Namen. Die automatische Zertifikatserneuerung des REST-Servers wird nicht benötigt.&lt;br /&gt;
* &#039;&#039;&#039;Weiterleitung:&#039;&#039;&#039; Der Proxy leitet per HTTPS an &amp;lt;code&amp;gt;https://&amp;lt;Server des REST-Dienstes&amp;gt;:&amp;lt;Port&amp;gt;&amp;lt;/code&amp;gt; weiter. Für diese Strecke genügt am REST-Server ein Zertifikat Ihrer internen Zertifizierungsstelle oder ein selbst signiertes, dem der Proxy vertraut.&lt;br /&gt;
* &#039;&#039;&#039;Header:&#039;&#039;&#039; Alle HTTP-Methoden und Header unverändert durchreichen, insbesondere &#039;&#039;Authorization&#039;&#039;, &#039;&#039;apikey&#039;&#039; und &#039;&#039;Idempotency-Key&#039;&#039;. Antworten nicht zwischenspeichern. Den Header &#039;&#039;X-Forwarded-For&#039;&#039; mit der IP-Adresse des Konsumenten setzen.&lt;br /&gt;
* &#039;&#039;&#039;Anfragegrösse:&#039;&#039;&#039; Die maximale Anfragegrösse am Proxy darf nicht unter der Upload-Grenze der Endpunkte liegen.&lt;br /&gt;
* &#039;&#039;&#039;Firewall:&#039;&#039;&#039; Den Port des REST-Servers intern nur für den Proxy freigeben.&lt;br /&gt;
&lt;br /&gt;
Hinter dem Proxy verhält sich der REST-Server so:&lt;br /&gt;
&lt;br /&gt;
* Protokoll und Konsole zeigen als Host die IP-Adresse des Proxys und hängen den Wert von &#039;&#039;X-Forwarded-For&#039;&#039; als &amp;lt;code&amp;gt;[xff=…]&amp;lt;/code&amp;gt; an.&lt;br /&gt;
* Die [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge#Host-Beschränkung|Host-Beschränkung]] eines Zugangs kann keine einzelnen Konsumenten unterscheiden.&lt;br /&gt;
* Beim [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile#Rate-Limit je Profil|Rate-Limit]] teilen sich alle Konsumenten ohne API-Key einen Zähler.&lt;br /&gt;
&lt;br /&gt;
==Wartung der Server-Tabellen==&lt;br /&gt;
&lt;br /&gt;
Die Tabellen des REST-Servers pflegen sich selbst; ein regelmässiger Eingriff ist&lt;br /&gt;
nicht vorgesehen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Tabelle !! Aufräumen&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_IDEMPOTENCY || Abgeschlossene Einträge werden nach 30 Tagen gelöscht. Offene Reservierungen werden ab 24 Stunden im Protokoll gemeldet und &#039;&#039;&#039;anschliessend ebenfalls gelöscht&#039;&#039;&#039; - sie sind trotzdem ein Support-Fall, weil die Fachwirkung unbekannt ist, siehe Stolpersteine&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_TOKEN || Zeilen werden gelöscht, sobald Access- &#039;&#039;&#039;und&#039;&#039;&#039; Refresh-Laufzeit vorbei sind. Läuft beim Serverstart und danach höchstens stündlich&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_PROTO / RESTSRV_STATS || Wachsen mit dem Betrieb. Bei hohem Aufkommen empfiehlt sich ein turnusmässiges Archivieren&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Häufige Stolpersteine==&lt;br /&gt;
&lt;br /&gt;
* Bindung angelegt, aber der Server wurde nicht neugestartet -&amp;gt; die Bindung ist noch nicht aktiv.&lt;br /&gt;
* Endpunkt einem falschen Server zugeordnet -&amp;gt; Aufruf liefert 404 &#039;&#039;Endpunkt nicht vorhanden oder inaktiv&#039;&#039;.&lt;br /&gt;
* Pfad-Template falsch geschrieben oder Segmentzahl passt nicht -&amp;gt; kein Template matcht, Aufruf liefert 404.&lt;br /&gt;
* Zugang für den Endpunkt nicht freigeschaltet -&amp;gt; Aufruf liefert 403 &#039;&#039;Keine Berechtigung für die Endpunkt-Nutzung&#039;&#039;.&lt;br /&gt;
* Falscher API-Key oder falscher Host beim Zugang -&amp;gt; Aufruf liefert 401 bzw. 403.&lt;br /&gt;
* mTLS aktiv, aber kein Client-Zertifikat installiert -&amp;gt; Verbindung wird während des TLS-Handshakes abgebrochen.&lt;br /&gt;
* Datei-Upload ohne gesetzten Haken &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; am Endpunkt -&amp;gt; Aufruf liefert 415, das Skript läuft nicht an.&lt;br /&gt;
* Skript geändert, aber der Aufruf verhält sich noch wie vorher -&amp;gt; Endpunkt-Cache abwarten (bis zu 60 Sekunden).&lt;br /&gt;
* Schreibaufruf (POST/PUT/PATCH/DELETE) liefert 400 &#039;&#039;IDEMPOTENCY_KEY_MISSING&#039;&#039; -&amp;gt; der Client sendet keinen &#039;&#039;Idempotency-Key&#039;&#039;. Der Header ist seit v1.21 &#039;&#039;&#039;Pflicht&#039;&#039;&#039;, nicht optional; das Endpunkt-Skript läuft in diesem Fall gar nicht an. Betrifft typischerweise Konsumenten, die vor dem Update gebaut wurden.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 422 &#039;&#039;IDEMPOTENCY_KEY_REUSED&#039;&#039; -&amp;gt; der Client sendet denselben &#039;&#039;Idempotency-Key&#039;&#039; mit geändertem Inhalt. Pro Vorgang genau ein Schlüssel, über alle Wiederholversuche hinweg unverändert.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 409 &#039;&#039;IDEMPOTENCY_UNRESOLVED&#039;&#039; -&amp;gt; ein früherer Versuch wurde hart abgebrochen (Dienst beendet). Ob gebucht wurde, ist offen: im Protokoll und in den Fachdaten prüfen. Der Aufräumlauf meldet den Eintrag nach 24 Stunden im Protokoll und löscht ihn dann; wer schneller weitermachen will, entfernt ihn in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;.&lt;br /&gt;
* Nach einem Update antworten &#039;&#039;&#039;alle&#039;&#039;&#039; bestehenden Token mit 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; -&amp;gt; erwartetes Verhalten. Token, die vor der Einführung der Sitzungsprüfung ausgestellt wurden, gehören zu keiner Sitzung; die Konsumenten müssen sich einmalig neu anmelden.&lt;br /&gt;
* Anmeldung liefert 503 statt eines Tokens -&amp;gt; die Sitzungszeile konnte nicht geschrieben werden. Ursache im Protokoll unter &#039;&#039;TokenStore: ...&#039;&#039; nachsehen; typisch ist eine fehlende Tabelle &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; nach einem Update ohne Datenbank-Abgleich.&lt;br /&gt;
* Abmelden liefert 503 -&amp;gt; die Sitzung liess sich nicht sperren. Der Client ist &#039;&#039;&#039;nicht&#039;&#039;&#039; abgemeldet und muss den Aufruf wiederholen; Details im Protokoll.&lt;br /&gt;
* Techniker wird beim Arbeiten unerwartet abgemeldet -&amp;gt; im Protokoll nach &#039;&#039;WIEDERVORLAGE&#039;&#039; suchen. Ein doppelt verwendetes Refresh-Token sperrt die Sitzung. Legt die App Token mehrfach parallel ab, gehört das clientseitig behoben.&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/%C3%96ffentliche_Freigabe&amp;diff=67072</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/%C3%96ffentliche_Freigabe&amp;diff=67072"/>
		<updated>2026-10-26T10:12:38Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Datenbank nicht zwingend auf dem Server des REST-Dienstes, Abschnitt REST-Dienst auf einem eigenen Server ergänzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
{{Seitentyp|Übersicht}}&lt;br /&gt;
= Öffentliche Freigabe =&lt;br /&gt;
&lt;br /&gt;
Apps auf mobilen Geräten und externe Systeme erreichen den REST-Server nur, wenn er aus dem Internet erreichbar ist. Diese Seite stellt die drei Wege dafür gegenüber und richtet sich an die IT-Administration, die über die Freigabe entscheidet.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der REST-Dienst läuft auf dem OBS-Server oder auf einem [[#REST-Dienst auf einem eigenen Server|eigenen Server]]. Auf dem OBS-Server läuft häufig auch die Datenbank. Die Varianten unterscheiden sich vor allem darin, ob eine Verbindung aus dem Internet den Server des REST-Dienstes direkt erreicht.}}&lt;br /&gt;
&lt;br /&gt;
== Varianten im Vergleich ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Merkmal !! Direkte Portfreigabe !! Reverse-Proxy !! VPN&lt;br /&gt;
|-&lt;br /&gt;
| Aus dem Internet erreichbar || Server des REST-Dienstes: Port des REST-Servers, bei Prüfart http-01 zusätzlich Port 80 || nur der Proxy (Port 443) || nur das VPN-Gateway&lt;br /&gt;
|-&lt;br /&gt;
| Aufwand für die Nutzer der App || keiner || keiner || VPN-Client einrichten und vor der Nutzung einschalten&lt;br /&gt;
|-&lt;br /&gt;
| Aufwand für die IT || Portweiterleitung am Router || Proxy betreiben, Zertifikat am Proxy || VPN-Zugang für jedes Gerät&lt;br /&gt;
|-&lt;br /&gt;
| Filtern und Protokollieren vor dem REST-Dienst || – || ✓ || nur auf Netzebene&lt;br /&gt;
|-&lt;br /&gt;
| Öffentliches Zertifikat || im REST-Server, Prüfart http-01 oder dns-01 || am Proxy || im REST-Server, Prüfart dns-01&lt;br /&gt;
|-&lt;br /&gt;
| REST-Server sieht die IP-Adresse des Geräts || ✓ || nur im Protokoll (über &#039;&#039;X-Forwarded-For&#039;&#039;) || ✓, sofern das VPN-Gateway die Adressen nicht umsetzt (NAT)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Schutz durch den REST-Server ==&lt;br /&gt;
Unabhängig von der Variante schützt sich der REST-Server selbst. Diese Mechanismen greifen in allen drei Varianten:&lt;br /&gt;
* &#039;&#039;&#039;Verschlüsselung:&#039;&#039;&#039; ausschliesslich TLS 1.2 mit Forward Secrecy und AEAD-Verfahren. Unverschlüsselte Anfragen an einen TLS-Port werden vor jeder Verarbeitung abgewiesen. Optional mTLS: jeder Client braucht ein gültiges Client-Zertifikat.&lt;br /&gt;
* &#039;&#039;&#039;Zugänge:&#039;&#039;&#039; Jede Anfrage braucht einen gültigen API-Key, wahlweise zusätzlich JWT. Jeder Zugang ist nur für die Endpunkte freigeschaltet, die er braucht, und lässt sich auf eine Absenderadresse beschränken. API-Keys lassen sich ohne Unterbrechung tauschen.&lt;br /&gt;
* &#039;&#039;&#039;Widerrufbare Sitzungen:&#039;&#039;&#039; Eine Abmeldung wirkt sofort. Ein Refresh-Token gilt genau einmal; wird ein verbrauchtes erneut vorgelegt, sperrt der Server die ganze Sitzung.&lt;br /&gt;
* &#039;&#039;&#039;Grenzen gegen Missbrauch:&#039;&#039;&#039; getrenntes Rate-Limit für erfolgreiche und fehlgeschlagene Anfragen, eine Obergrenze gleichzeitiger Anfragen und Verbindungen sowie Grössengrenzen für Anfragen und Uploads.&lt;br /&gt;
* &#039;&#039;&#039;Keine Informationen nach aussen:&#039;&#039;&#039; Fehlerantworten enthalten nie interne Details. Die stehen nur im Protokoll, mit einer Korrelations-ID je Anfrage. Kennwörter und Token werden dort maskiert.&lt;br /&gt;
* &#039;&#039;&#039;Kurze Öffnung für die Zertifikatsprüfung:&#039;&#039;&#039; Bei http-01 öffnet der Dienst Port 80 nur für die Dauer der Prüfung.&lt;br /&gt;
&lt;br /&gt;
Diese Mechanismen schützen den REST-Dienst. Reverse-Proxy und VPN fügen eine Schicht davor hinzu: Sie halten den Server des REST-Dienstes aus dem Internet heraus, und mit ihm häufig auch den OBS-Server und die Datenbank. Das wirkt auch gegen Schwachstellen ausserhalb des REST-Servers und gegen Überlastung.&lt;br /&gt;
Der Router leitet den Port des REST-Servers aus dem Internet auf den Server des REST-Dienstes weiter. Für die Zertifikatserneuerung mit der Prüfart http-01 wird zusätzlich Port 80 weitergeleitet.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Die einfachste Einrichtung, es wird keine weitere Komponente gebraucht.&lt;br /&gt;
* Die App funktioniert überall, ohne dass die Nutzer etwas tun müssen.&lt;br /&gt;
* Der REST-Server sieht die IP-Adresse jedes Geräts. Alle Schutzmechanismen des REST-Servers, auch Host-Beschränkung und Rate-Limit, arbeiten damit je Gerät.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Der Server des REST-Dienstes ist direkt aus dem Internet erreichbar. Läuft der REST-Dienst auf dem OBS-Server, gilt das für den OBS-Server und häufig auch für die Datenbank. Die [[#Schutz durch den REST-Server|Schutzmechanismen des REST-Servers]] greifen, eine zusätzliche Schicht davor gibt es aber nicht.&lt;br /&gt;
* Mit der Prüfart http-01 ist zusätzlich Port 80 weitergeleitet. Mit der Prüfart dns-01 entfällt das.&lt;br /&gt;
&lt;br /&gt;
== Reverse-Proxy ==&lt;br /&gt;
Ein Reverse-Proxy, zum Beispiel in einer DMZ oder als Funktion der Firewall, nimmt HTTPS auf Port 443 an. Er prüft die Anfrage und reicht sie über eine eigene, ebenfalls verschlüsselte Verbindung an den REST-Server weiter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Der Server des REST-Dienstes ist aus dem Internet nicht erreichbar, nur der Proxy.&lt;br /&gt;
* Am Proxy lassen sich Anfragen nach Ihren Vorgaben filtern, drosseln und protokollieren.&lt;br /&gt;
* Für die Nutzer der App ändert sich nichts.&lt;br /&gt;
* Das öffentliche Zertifikat liegt am Proxy. Die Zertifikatserneuerung des REST-Servers wird nicht gebraucht.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Der Proxy muss betrieben und gepflegt werden.&lt;br /&gt;
* Der REST-Server sieht als Absender nur den Proxy. Die Host-Beschränkung eines Zugangs kann keine Geräte unterscheiden, und Anfragen ohne API-Key teilen sich einen Zähler des Rate-Limits. Die IP-Adresse des Geräts steht nur im Protokoll, wenn der Proxy den Header &#039;&#039;X-Forwarded-For&#039;&#039; setzt.&lt;br /&gt;
&lt;br /&gt;
Die Einrichtung beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung#Betrieb hinter einem Reverse-Proxy|Betrieb hinter einem Reverse-Proxy]].&lt;br /&gt;
&lt;br /&gt;
== VPN ==&lt;br /&gt;
Die Geräte verbinden sich per VPN mit dem Netz, in dem der REST-Dienst läuft. Die App spricht durch den Tunnel per HTTPS mit dem REST-Server. Aus dem Internet ist nur das VPN-Gateway erreichbar.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Für den REST-Server ist kein Port aus dem Internet offen.&lt;br /&gt;
* Nur Geräte mit VPN-Zugang erreichen den REST-Server.&lt;br /&gt;
* Setzt das VPN-Gateway die Adressen nicht um, sieht der REST-Server die VPN-Adresse jedes Geräts.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Jedes Gerät braucht einen VPN-Client und einen eigenen Zugang. Ohne zentrale Geräteverwaltung schalten die Nutzer das VPN von Hand ein. Ist es aus, meldet die App, dass der Server nicht erreichbar ist.&lt;br /&gt;
* Ein VPN-Zugang öffnet den Weg ins Netz. Beschränken Sie ihn auf die Adresse und den Port des REST-Servers.&lt;br /&gt;
* Das Zertifikat lässt sich nur mit der Prüfart dns-01 erneuern, weil die Zertifizierungsstelle den Server nicht erreicht.&lt;br /&gt;
* Der Name des REST-Servers muss auf dem Gerät auf die interne Adresse zeigen. Das geht über einen DNS-Server, den das VPN mitliefert, oder über einen öffentlichen DNS-Eintrag mit der internen Adresse. Manche Router verwerfen solche Antworten als Schutz gegen DNS-Rebinding; dann löst der Name in diesem Netz nicht auf.&lt;br /&gt;
&lt;br /&gt;
Das Zertifikat richten Sie wie unter [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]] beschrieben mit der Prüfart dns-01 ein.&lt;br /&gt;
&lt;br /&gt;
== REST-Dienst auf einem eigenen Server ==&lt;br /&gt;
Der REST-Dienst muss nicht auf dem OBS-Server laufen. Er lässt sich auch auf einem eigenen Server betreiben, zum Beispiel in einer DMZ. Voraussetzung ist, dass dieser Server&lt;br /&gt;
* per SMB auf den OBS-Server zugreifen kann und&lt;br /&gt;
* die Datenbank erreicht.&lt;br /&gt;
&lt;br /&gt;
Der eigene Server lässt sich mit jeder der drei Varianten kombinieren. Bei der direkten Portfreigabe zeigt die Weiterleitung dann auf diesen Server statt auf den OBS-Server.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Eine Verbindung aus dem Internet erreicht weder den OBS-Server noch die Datenbank direkt, auch bei direkter Portfreigabe.&lt;br /&gt;
* Die Verarbeitung der Anfragen läuft nicht auf dem OBS-Server. Die Datenbank wird weiterhin belastet.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Ein weiterer Server muss betrieben und gepflegt werden.&lt;br /&gt;
* Vom eigenen Server ins interne Netz müssen SMB und der Port der Datenbank offen sein. Beschränken Sie diese Wege auf den OBS-Server und den Datenbankserver.&lt;br /&gt;
&lt;br /&gt;
== Entscheidungshilfe ==* &#039;&#039;&#039;Keine eingehende Verbindung zum OBS-Server erlaubt, DMZ oder Firewall mit Proxy-Funktion vorhanden:&#039;&#039;&#039; Reverse-Proxy.&lt;br /&gt;
* &#039;&#039;&#039;Keine eingehende Verbindung zum OBS-Server erlaubt, aber kein Proxy vorhanden:&#039;&#039;&#039; REST-Dienst auf einem eigenen Server in einer DMZ, mit direkter Portfreigabe auf diesen Server.&lt;br /&gt;
* &#039;&#039;&#039;Geräte werden zentral verwaltet oder ein VPN ist ohnehin im Einsatz:&#039;&#039;&#039; VPN.&lt;br /&gt;
* &#039;&#039;&#039;Kleine Installation ohne Vorgaben der IT:&#039;&#039;&#039; Direkte Portfreigabe, möglichst mit der Prüfart dns-01, damit Port 80 geschlossen bleibt.&lt;br /&gt;
&lt;br /&gt;
== Siehe auch ==&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge#Host-Beschränkung|Host-Beschränkung]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zertifikatserneuerung&amp;diff=67068</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zertifikatserneuerung&amp;diff=67068"/>
		<updated>2026-10-26T09:47:31Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Beispiel-Domain firma.de statt kunde.de [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
{{Seitentyp|Anleitung}}&lt;br /&gt;
= Zertifikatserneuerung =&lt;br /&gt;
&lt;br /&gt;
Der REST-Server kann sein TLS-Zertifikat selbst bei der Zertifizierungsstelle Let&#039;s Encrypt beantragen und vor Ablauf erneuern. Zertifikat und privater Schlüssel entstehen auf Ihrem Server und verlassen ihn nicht. Diese Seite beschreibt, wie Sie die Erneuerung je nach Netzaufbau einrichten.&lt;br /&gt;
&lt;br /&gt;
{{Auf einen Blick|1=&lt;br /&gt;
* &#039;&#039;&#039;Zwei Prüfarten.&#039;&#039;&#039; http-01 prüft über Port 80, dns-01 über einen DNS-Eintrag und braucht keinen offenen Port.&lt;br /&gt;
* &#039;&#039;&#039;Selbsttest vorab.&#039;&#039;&#039; Vor dem ersten Zertifikat und danach wöchentlich prüft der Dienst den Weg über das Testsystem der Zertifizierungsstelle.&lt;br /&gt;
* &#039;&#039;&#039;Erneuerung ab 30 Tagen Restlaufzeit.&#039;&#039;&#039; Das bisherige Zertifikat bleibt als Rückfall erhalten.&lt;br /&gt;
* &#039;&#039;&#039;dns-01 braucht nur ausgehende Verbindungen&#039;&#039;&#039; und eignet sich damit für VPN und für Server ohne Portfreigabe.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Voraussetzungen ==&lt;br /&gt;
* Ein Name, auf den das Zertifikat lautet, zum Beispiel &#039;&#039;obs.firma.de&#039;&#039;. Platzhalter-Zertifikate (&#039;&#039;*.firma.de&#039;&#039;) werden nicht unterstützt.&lt;br /&gt;
* Ein Server-Profil mit TLS, also ohne Haken &#039;&#039;&#039;kein SSL&#039;&#039;&#039;.&lt;br /&gt;
* Ausgehende HTTPS-Verbindungen zur Zertifizierungsstelle (&amp;lt;code&amp;gt;acme-v02.api.letsencrypt.org&amp;lt;/code&amp;gt; und für den Selbsttest &amp;lt;code&amp;gt;acme-staging-v02.api.letsencrypt.org&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Bei dns-01 zusätzlich ausgehende HTTPS-Verbindungen zur Schnittstelle des DNS-Anbieters bzw. zur acme-dns-Instanz.&lt;br /&gt;
&lt;br /&gt;
=== Welche Prüfart? ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Prüfart !! Funktioniert, wenn !! Aus dem Internet erreichbar sein muss&lt;br /&gt;
|-&lt;br /&gt;
| http-01 || der Name öffentlich auf diesen Server zeigt und Port 80 von aussen auf ihn weitergeleitet ist || Port 80&lt;br /&gt;
|-&lt;br /&gt;
| dns-01 || sich der DNS-Eintrag &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; über eine Schnittstelle schreiben lässt, direkt oder über einen CNAME || nichts&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Welche Prüfart zu Ihrer [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|öffentlichen Freigabe]] passt:&lt;br /&gt;
* &#039;&#039;&#039;Direkte Portfreigabe:&#039;&#039;&#039; http-01 oder dns-01. Mit dns-01 bleibt Port 80 geschlossen.&lt;br /&gt;
* &#039;&#039;&#039;Reverse-Proxy:&#039;&#039;&#039; Das öffentliche Zertifikat liegt am Proxy, die Erneuerung im REST-Server wird nicht gebraucht.&lt;br /&gt;
* &#039;&#039;&#039;VPN:&#039;&#039;&#039; dns-01.&lt;br /&gt;
&lt;br /&gt;
=== Wege bei dns-01 ===&lt;br /&gt;
Bei dns-01 fragt die Zertifizierungsstelle den TXT-Eintrag &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; ab. Der Dienst schreibt ihn vor der Prüfung und löscht ihn danach wieder. Dafür gibt es drei Wege:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Weg !! Wann !! Einmalig beim DNS des Namens !! Einstellung &#039;&#039;&#039;DNS-Anbieter&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Direkt beim Anbieter || Die Zone des Namens liegt bei Hetzner, Cloudflare oder IONOS || nichts, Sie brauchen nur ein API-Token || &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| CNAME auf eine Zone bei einem Anbieter || Die Zone des Namens liegt bei einem anderen Anbieter, oder im REST-Server sollen keine Zugangsdaten zur eigenen Zone liegen || CNAME &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf einen Namen in einer Zone bei Hetzner, Cloudflare oder IONOS || &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| CNAME auf acme-dns || Eine acme-dns-Instanz steht zur Verfügung, im eigenen Betrieb oder bei einem Dienstleister || CNAME &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf die &#039;&#039;fulldomain&#039;&#039; des acme-dns-Kontos || &#039;&#039;acme-dns&#039;&#039;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim CNAME-Weg genügt beim DNS-Anbieter des Namens ein von Hand angelegter Eintrag, auch bei Anbietern ohne Schnittstelle. Der REST-Server bekommt dann nur Zugang zur Zone, auf die der CNAME zeigt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Namen von DynDNS-Diensten (zum Beispiel &#039;&#039;…myfritz.net&#039;&#039;) eignen sich in der Regel nicht als Zertifikatsname, weil sich dort keine eigenen Einträge anlegen lassen. Verwenden Sie einen Namen unter Ihrer eigenen Domain. Der DynDNS-Name bleibt davon unberührt, zum Beispiel für den Zugang zum VPN.}}&lt;br /&gt;
&lt;br /&gt;
== Vorgehen ==&lt;br /&gt;
=== Gemeinsame Einstellungen ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Wählen Sie &#039;&#039;&#039;Stammdaten → Z Weitere Stammdaten → REST-Server → Server&#039;&#039;&#039; und öffnen Sie das Server-Profil.&lt;br /&gt;
# Stellen Sie auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039;.&lt;br /&gt;
# Tragen Sie im Feld &#039;&#039;&#039;Zertifikatsname (FQDN)&#039;&#039;&#039; den Namen ein, auf den das Zertifikat lauten soll.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Kontakt bei der CA&#039;&#039;&#039; eine E-Mail-Adresse ein. Die Zertifizierungsstelle schickt dorthin Warnungen, wenn ein Zertifikat abzulaufen droht.&lt;br /&gt;
# Richten Sie die Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; nach einem der folgenden Abschnitte ein und speichern Sie.&lt;br /&gt;
# Starten Sie den REST-Dienst neu.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart http-01 ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Lassen Sie den Namen öffentlich auf die Adresse Ihres Internetanschlusses zeigen.&lt;br /&gt;
# Leiten Sie am Router Port 80 von aussen auf den OBS-Server weiter.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; im Feld &#039;&#039;&#039;Prüfart&#039;&#039;&#039; den Eintrag &#039;&#039;http-01 (Port 80)&#039;&#039;. Die übrigen Felder bleiben leer.&lt;br /&gt;
# Geben Sie in der Konsole des REST-Servers &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Port 80 darf auf dem OBS-Server von keinem anderen Dienst belegt sein, häufig ist das ein Webserver wie IIS. Der REST-Server öffnet den Port nur für die Dauer der Prüfung und schliesst ihn danach wieder.&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 direkt beim DNS-Anbieter ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Erstellen Sie beim Anbieter der Zone ein API-Token:&lt;br /&gt;
#* &#039;&#039;&#039;Hetzner:&#039;&#039;&#039; ein API-Token des Projekts mit Lese- und Schreibrecht in der Hetzner Console. Zonen in der früheren DNS-Konsole (dns.hetzner.com) werden nicht unterstützt.&lt;br /&gt;
#* &#039;&#039;&#039;Cloudflare:&#039;&#039;&#039; ein API-Token mit der Berechtigung, DNS zu bearbeiten, beschränkt auf die Zone.&lt;br /&gt;
#* &#039;&#039;&#039;IONOS:&#039;&#039;&#039; einen API-Schlüssel. Er besteht aus Präfix und Secret.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; im Feld &#039;&#039;&#039;Prüfart&#039;&#039;&#039; den Eintrag &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und im Feld &#039;&#039;&#039;DNS-Anbieter&#039;&#039;&#039; den Anbieter.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; das Token ein, bei IONOS in der Form &amp;lt;code&amp;gt;Präfix.Secret&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Lassen Sie &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; leer.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 mit CNAME auf eine Zone bei einem DNS-Anbieter ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Legen Sie in einer Zone bei Hetzner, Cloudflare oder IONOS einen Namen für die Prüfung fest, zum Beispiel &#039;&#039;obs-firma.acme.example.de&#039;&#039;. Einen Eintrag müssen Sie dort nicht anlegen.&lt;br /&gt;
# Legen Sie beim DNS-Anbieter Ihres Namens einen CNAME an: &amp;lt;code&amp;gt;_acme-challenge.obs.firma.de&amp;lt;/code&amp;gt; auf &#039;&#039;obs-firma.acme.example.de&#039;&#039;.&lt;br /&gt;
# Erstellen Sie ein API-Token für die Zone, in der das Ziel liegt, wie im vorigen Abschnitt beschrieben.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; die Prüfart &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und den Anbieter dieser Zone.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; das Token ein.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; das Ziel des CNAME ein, hier &#039;&#039;obs-firma.acme.example.de&#039;&#039;.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 mit CNAME auf acme-dns ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; die Prüfart &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und den DNS-Anbieter &#039;&#039;acme-dns&#039;&#039;.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Adresse (acme-dns)&#039;&#039;&#039; die Adresse der Instanz ein. Sie beginnt mit &amp;lt;code&amp;gt;https://&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Lassen Sie &#039;&#039;&#039;Benutzer (acme-dns)&#039;&#039;&#039;, &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; und &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; leer und speichern Sie.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein. Der Dienst legt das Konto bei acme-dns an und trägt Benutzer, Schlüssel und Name selbst ein. Unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; steht danach der CNAME, den Sie anlegen müssen.&lt;br /&gt;
# Legen Sie beim DNS-Anbieter Ihres Namens diesen CNAME an: &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf die genannte &#039;&#039;fulldomain&#039;&#039;.&lt;br /&gt;
# Geben Sie in der Konsole erneut &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Leeren Sie die drei Felder später nicht. Der Dienst würde ein neues Konto mit einer neuen &#039;&#039;fulldomain&#039;&#039; anlegen, und der gesetzte CNAME zeigte ins Leere. Ein bestehendes Konto tragen Sie mit allen drei Angaben ein. Teilweise gefüllte Angaben weist die Maske beim Speichern ab.}}&lt;br /&gt;
&lt;br /&gt;
=== Erstes Zertifikat holen ===&lt;br /&gt;
Ist der Selbsttest bestanden, holt der Dienst das Zertifikat bei der nächsten stündlichen Prüfung. Sofort geht es mit &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; in der Konsole. Bis das erste Zertifikat da ist, kann sich kein Client mit dem Server-Profil verbinden.&lt;br /&gt;
&lt;br /&gt;
=== Testbetrieb ===&lt;br /&gt;
{{Achtung|Im &#039;&#039;&#039;Testbetrieb&#039;&#039;&#039; beantragt der Dienst das Zertifikat beim Testsystem der Zertifizierungsstelle. Einem solchen Zertifikat vertraut &#039;&#039;&#039;kein&#039;&#039;&#039; Client, Anwendungen können sich danach nicht mehr mit diesem Server verbinden. Verwenden Sie den Testbetrieb ausschliesslich auf Testsystemen. Zurück in den Produktivbetrieb kommen Sie, indem Sie das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039; stellen. Der Dienst holt daraufhin sofort ein gültiges Zertifikat, auch wenn das Testzertifikat noch lange läuft.}}&lt;br /&gt;
&lt;br /&gt;
== Ergebnis ==&lt;br /&gt;
Nach bestandenem Selbsttest ist auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; der Haken &#039;&#039;&#039;Selbsttest bestanden&#039;&#039;&#039; gesetzt. Unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; steht &amp;quot;Selbsttest bestanden - von aussen erreichbar&amp;quot; (http-01) bzw. &amp;quot;Selbsttest bestanden - DNS-Eintrag von der CA geprueft&amp;quot; (dns-01). Nach der Ausstellung zeigt &#039;&#039;&#039;Zertifikat gültig bis&#039;&#039;&#039; das Ablaufdatum, und &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; in der Konsole zeigt den Stand je Profil.&lt;br /&gt;
&lt;br /&gt;
Danach läuft die Erneuerung selbständig:&lt;br /&gt;
# &#039;&#039;&#039;Prüfung:&#039;&#039;&#039; Der Dienst sieht stündlich nach, ob etwas ansteht.&lt;br /&gt;
# &#039;&#039;&#039;Selbsttest:&#039;&#039;&#039; Einmal wöchentlich und nach jeder Änderung von Zertifikatsname, Prüfart, DNS-Anbieter oder Name des TXT-Eintrags prüft der Dienst den Weg über das Testsystem der Zertifizierungsstelle. Besteht der Test nicht, beantragt er kein Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Erneuerung:&#039;&#039;&#039; Ab 30 Tagen Restlaufzeit beantragt der Dienst ein neues Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Übernahme:&#039;&#039;&#039; Der Dienst prüft das neue Zertifikat, speichert es und bewahrt das bisherige als Rückfall auf. Anschliessend startet nur das betroffene Server-Profil neu.&lt;br /&gt;
# &#039;&#039;&#039;Rückfall:&#039;&#039;&#039; Startet das Profil mit dem neuen Zertifikat nicht, aktiviert der Dienst wieder das vorherige.&lt;br /&gt;
&lt;br /&gt;
Nach einem Fehlschlag wartet der Dienst zunehmend länger, bevor er es erneut versucht: eine, zwei, vier, acht und höchstens 24 Stunden. Das Feld &#039;&#039;&#039;Gesperrt bis&#039;&#039;&#039; zeigt, bis wann. &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; in der Konsole wirken sofort, auch während der Sperre.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; || Zeigt je Profil Betriebsart, Ablaufdatum, Zustand und das Ergebnis des letzten Selbsttests&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; || Prüft sofort den Weg über das Testsystem der Zertifizierungsstelle. Beantragt kein Zertifikat&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; || Stösst die Erneuerung sofort an, auch wenn sie noch nicht fällig ist&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Bei dns-01 wartet der Dienst nach dem Schreiben des Eintrags 30 Sekunden und dann bis zu drei Minuten, bis der Eintrag über öffentliche DNS-Server sichtbar ist. Erst dann löst er die Prüfung aus.&lt;br /&gt;
&lt;br /&gt;
== Wenn es nicht klappt ==&lt;br /&gt;
Die Meldungen stehen unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; und im Protokoll des REST-Servers.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Meldung !! Ursache&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Port 80 laesst sich nicht oeffnen - er ist belegt&amp;quot; || Ein anderer Dienst auf dem OBS-Server belegt Port 80, häufig IIS. Dienst abschalten oder die Prüfart dns-01 verwenden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;dns-01 ist eingestellt, aber kein DNS-Anbieter&amp;quot; || Auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; ist kein DNS-Anbieter gewählt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;kein API-Token&amp;quot; || Das Feld &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; ist leer&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;acme-dns: Konto unvollstaendig&amp;quot; || Von Benutzer, Schlüssel und Name des TXT-Eintrags ist nur ein Teil gefüllt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Hetzner: keine Zone zu … im Projekt des Tokens&amp;quot; || Das Token gehört zu einem anderen Projekt, oder die Zone liegt noch in der früheren DNS-Konsole&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Cloudflare: keine Zone zu …, die das Token sehen darf&amp;quot; || Das Token ist nicht für diese Zone freigegeben&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;IONOS: keine Zone zu … im Vertrag des Schluessels&amp;quot; || Der API-Schlüssel gehört zu einem anderen Vertrag&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;… lehnt den TXT-Eintrag ab (HTTP 401)&amp;quot; oder &amp;quot;(HTTP 403)&amp;quot; || Das Token ist ungültig oder darf keine DNS-Einträge ändern&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;… ist nach 210 Sekunden noch nicht ueberall sichtbar&amp;quot; || Der Eintrag ist über öffentliche DNS-Server nicht zu sehen. Meist fehlt der CNAME oder zeigt auf ein anderes Ziel als &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039;. Der Dienst löst die Prüfung trotzdem aus&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;kein oeffentlicher DNS-Server erreichbar&amp;quot; || Ausgehendes DNS (Port 53) ist gesperrt. Der Dienst kann die Sichtbarkeit nicht prüfen und wartet fest 60 Sekunden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;acme-dns-Konto angelegt - jetzt CNAME … setzen&amp;quot; || Kein Fehler: Legen Sie den genannten CNAME an und geben Sie &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Siehe auch ==&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile#Automatische Zertifikatserneuerung|Server-Profile: Felder der Zertifikatserneuerung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zugaenge&amp;diff=67071</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Zugaenge</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zugaenge&amp;diff=67071"/>
		<updated>2026-10-26T09:47:30Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Beispiel-Domains und mTLS-Beispiele: firma.de statt kunde.de [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Zugänge=&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Zugang&#039;&#039;&#039; (Account) ist der Kanal, über den ein externer Konsument den REST-Server anspricht. Er bündelt API-Key, Host-Beschränkung, CORS-Konfiguration, optionale JWT-Authentifizierung und optionale mTLS-Subject-Prüfung.&lt;br /&gt;
&lt;br /&gt;
==Aufruf==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Zugänge&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Felder==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld          !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Name          || Sprechender Name, erscheint im Protokoll und der Statistik&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv         || Inaktive Zugänge werden bei der Authentifizierung mit 403 abgewiesen&lt;br /&gt;
|-&lt;br /&gt;
| API-Key       || Eindeutiger Schlüssel, den der Konsument im HTTP-Header &#039;&#039;apikey&#039;&#039; senden muss. Per Schlüsselsymbol generierbar&lt;br /&gt;
|-&lt;br /&gt;
| Host          || Optionaler Hostname oder IP-Adresse des Konsumenten. Stimmt die anfragende IP nicht überein, wird mit 403 abgelehnt&lt;br /&gt;
|-&lt;br /&gt;
| CORS-Origins  || Liste erlaubter Origins für Browser-Aufrufe, mehrere durch &#039;&#039;;&#039;&#039; getrennt. &#039;&#039;&#039;Leer heisst nicht „keine Einschränkung&amp;quot;, sondern „kein Browser-Zugriff&amp;quot;&#039;&#039;&#039; - siehe unten&lt;br /&gt;
|-&lt;br /&gt;
| mTLS-Subject  || Erwartete RDN-Komponente(n) im Client-Zertifikat-Subject, Komma-/Semikolon-getrennt (alle müssen vorkommen; nur sinnvoll bei mTLS-Servern)&lt;br /&gt;
|-&lt;br /&gt;
| JWT           || Schalter, ob für diesen Zugang ein JWT-Token nötig ist&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Endpunkt  || Pfad, über den &#039;&#039;&#039;alle Token-Vorgänge&#039;&#039;&#039; laufen: Anmelden, Erneuern und Abmelden (z.B. &#039;&#039;oauth&#039;&#039; oder &#039;&#039;meineapp/dev/auth&#039;&#039;). Verglichen wird der &#039;&#039;&#039;vollständige Pfad&#039;&#039;&#039;, mehrstufige Angaben mit &#039;&#039;/&#039;&#039; sind also erlaubt&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Key       || Geheimer Schlüssel zur Signierung/Verifikation, per Schlüsselsymbol generierbar&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Exp       || Gültigkeitsdauer eines ausgestellten Access-Tokens in &#039;&#039;&#039;Minuten&#039;&#039;&#039;. Derselbe Wert wird dem Konsumenten in der Token-Antwort als &#039;&#039;expiresIn&#039;&#039; in &#039;&#039;&#039;Sekunden&#039;&#039;&#039; mitgeteilt und steht dem Endpunkt-Skript als &#039;&#039;_OBS_JWT_EXP_SEC&#039;&#039; zur Verfügung&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==API-Key==&lt;br /&gt;
&lt;br /&gt;
Der API-Key ist die Basis-Authentifizierung. Der Client übergibt ihn im HTTP-Header:&lt;br /&gt;
&lt;br /&gt;
 apikey: abcd-efgh-ijkl-mnop-qrstuvwxyz12&lt;br /&gt;
&lt;br /&gt;
Wird ein Aufruf ohne &#039;&#039;apikey&#039;&#039;-Header gestellt, sucht der Server intern nach einem Zugang mit API-Key &#039;&#039;&#039;*&#039;&#039;&#039; - damit lassen sich (mit Vorsicht!) auch öffentliche Endpunkte ohne Authentifizierung realisieren.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein Zugang mit API-Key &#039;&#039;*&#039;&#039; sollte nur für bewusst freigegebene Endpunkte verwendet werden und immer in Kombination mit einer Host-Beschränkung oder CORS-Beschränkung betrieben werden.}}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Nur für wirklich öffentliche Dateien.&#039;&#039;&#039; Ohne API-Key kennt der&lt;br /&gt;
Endpunkt weder Mandant noch Rolle - es gibt nichts, woran er eine Berechtigung&lt;br /&gt;
festmachen könnte ausser dem, was in der URL steht. Ein Wildcard-Zugang eignet&lt;br /&gt;
sich deshalb für Artefakte, die für jeden bestimmt sind (Test-APK, Handbuch),&lt;br /&gt;
&#039;&#039;&#039;nicht&#039;&#039;&#039; für Kundenbelege. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]].}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Bei einem Wildcard-Zugang sollte &#039;&#039;CORS-Origin&#039;&#039; &#039;&#039;&#039;leer&#039;&#039;&#039; bleiben.&lt;br /&gt;
Ein &#039;&#039;*&#039;&#039; dort wirkt auf die Preflight-Antwort &#039;&#039;&#039;aller&#039;&#039;&#039; Zugänge (siehe unten);&lt;br /&gt;
ein Download über die Adresszeile des Browsers sendet ohnehin kein &#039;&#039;Origin&#039;&#039;,&lt;br /&gt;
CORS greift dort also gar nicht.}}&lt;br /&gt;
&lt;br /&gt;
==Host-Beschränkung==&lt;br /&gt;
&lt;br /&gt;
Ist im Feld &#039;&#039;&#039;Host&#039;&#039;&#039; ein Wert hinterlegt, akzeptiert der Server Anfragen unter diesem Zugang nur dann, wenn die Remote-IP exakt diesem Host entspricht oder durch DNS-Auflösung auf diese IP zeigt. DNS-Auflösungen werden 5 Minuten gecached.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Hinter einem Reverse-Proxy ist die Remote-IP immer die des Proxys. Die Host-Beschränkung kann dort keine einzelnen Konsumenten unterscheiden. Der Header &#039;&#039;X-Forwarded-For&#039;&#039; wird dafür bewusst nicht ausgewertet, weil jeder Client ihn selbst setzen kann.}}&lt;br /&gt;
&lt;br /&gt;
==CORS==&lt;br /&gt;
&lt;br /&gt;
Soll ein Endpunkt aus einer Browser-Anwendung (Single-Page-App, JavaScript) heraus aufgerufen werden, muss das Origin der Anwendung in der CORS-Liste des Zugangs eingetragen sein.&lt;br /&gt;
&lt;br /&gt;
Format der Liste:&lt;br /&gt;
&lt;br /&gt;
 https://app.firma.de;https://test.firma.de;https://localhost:8080&lt;br /&gt;
&lt;br /&gt;
Ablauf:&lt;br /&gt;
&lt;br /&gt;
# Browser sendet einen Preflight-Request (&#039;&#039;OPTIONS&#039;&#039;). Der REST-Server prüft das Origin gegen die globale Origin-Liste &#039;&#039;&#039;aller&#039;&#039;&#039; Accounts (Cache, TTL 60s) und antwortet mit 204 + CORS-Header, falls erlaubt.&lt;br /&gt;
# Der eigentliche Request wird gegen die Origin-Liste &#039;&#039;&#039;des aktuell verwendeten Zugangs&#039;&#039;&#039; geprüft.&lt;br /&gt;
# Stimmt das Origin nicht, wird mit 403 &#039;&#039;Origin nicht erlaubt für Account ...&#039;&#039; abgelehnt.&lt;br /&gt;
&lt;br /&gt;
Der Preflight kann nicht zugangsbezogen prüfen: der Browser schickt dabei keinen&lt;br /&gt;
&#039;&#039;apikey&#039;&#039; mit, der Zugang ist zu diesem Zeitpunkt unbekannt. &#039;&#039;&#039;Die Durchsetzung&lt;br /&gt;
liegt deshalb auf Schritt 2&#039;&#039;&#039;, nicht auf dem Preflight. Praktische Folge: trägt&lt;br /&gt;
&#039;&#039;&#039;ein&#039;&#039;&#039; Zugang auf dem Server ein &#039;&#039;*&#039;&#039; in seiner Liste, antwortet jeder&lt;br /&gt;
Preflight mit &#039;&#039;Access-Control-Allow-Origin: *&#039;&#039; - auch für Zugänge mit engerer&lt;br /&gt;
Liste. Deren eigentliche Anfrage wird trotzdem gegen die eigene Liste geprüft.&lt;br /&gt;
&lt;br /&gt;
Erlaubte Methoden: &#039;&#039;GET, POST, PUT, DELETE, PATCH&#039;&#039; (systemseitig festgelegt).&lt;br /&gt;
&lt;br /&gt;
Bei den &#039;&#039;&#039;Anfrage-Headern&#039;&#039;&#039; spiegelt der Server im Preflight zurück, was der&lt;br /&gt;
Browser in &#039;&#039;Access-Control-Request-Headers&#039;&#039; angefragt hat. Damit sind eigene&lt;br /&gt;
Header eines Projekts - &#039;&#039;Idempotency-Key&#039;&#039;, &#039;&#039;If-Match&#039;&#039;, später&lt;br /&gt;
&#039;&#039;Upload-Offset&#039;&#039;/&#039;&#039;Upload-Length&#039;&#039; - ohne Änderung am Server nutzbar. Fragt der&lt;br /&gt;
Preflight keine Header an, antwortet der Server mit der Liste&lt;br /&gt;
&#039;&#039;Content-Type, apikey, Authorization, Accept&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Die Header-Liste ist &#039;&#039;&#039;keine Schutzgrenze&#039;&#039;&#039; - das ist das Origin. Sie&lt;br /&gt;
sagt dem Browser nur, welche Header der Server versteht. Dazu kommt, dass die&lt;br /&gt;
Zugangsdaten dieser Schnittstelle in Headern stehen (&#039;&#039;apikey&#039;&#039;,&lt;br /&gt;
&#039;&#039;Authorization&#039;&#039;) und nicht in Cookies: es gibt also keine automatisch&lt;br /&gt;
mitgeschickte Berechtigung, die eine fremde Seite ausnutzen könnte.}}&lt;br /&gt;
&lt;br /&gt;
===Was „leer&amp;quot; bedeutet===&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Ein &#039;&#039;&#039;leeres&#039;&#039;&#039; Feld CORS-Origins heisst &#039;&#039;&#039;nicht&#039;&#039;&#039; „keine&lt;br /&gt;
Einschränkung&amp;quot;. Es heisst: &#039;&#039;&#039;jeder Aufruf mit &#039;&#039;Origin&#039;&#039;-Header wird mit 403&lt;br /&gt;
abgelehnt&#039;&#039;&#039;. Für Clients &#039;&#039;&#039;ohne&#039;&#039;&#039; &#039;&#039;Origin&#039;&#039;-Header - mobile Apps, Server,&lt;br /&gt;
curl - ändert das Feld gar nichts, sie werden nie gegen CORS geprüft.}}&lt;br /&gt;
&lt;br /&gt;
Daraus ergeben sich drei sinnvolle Einstellungen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| leer || Kein Browser-Zugriff. Richtig für Zugänge, die nur von Apps oder Servern benutzt werden&lt;br /&gt;
|-&lt;br /&gt;
| konkrete Origins || Browser-Zugriff nur von diesen Seiten. Der Regelfall für Web-Anwendungen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;*&#039;&#039; || Browser-Zugriff von überall. Wirkt zusätzlich auf die Preflight-Antwort aller anderen Zugänge - bewusst und sparsam verwenden&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Für eine Browser-Anwendung ist dieses Feld besonders wichtig, weil ihr API-Key&lt;br /&gt;
zwangsläufig im JavaScript liegt und damit öffentlich ist. Die Origin-Liste&lt;br /&gt;
verhindert dann, dass jemand den Key nimmt und von seiner eigenen Seite benutzt.&lt;br /&gt;
Sie ist allerdings kein Ersatz für eine Zugriffskontrolle: wer den Key hat,&lt;br /&gt;
umgeht CORS mit einem einzigen Aufruf ausserhalb des Browsers.&lt;br /&gt;
&lt;br /&gt;
==JWT-Authentifizierung==&lt;br /&gt;
&lt;br /&gt;
Wird ein Zugang mit aktiver JWT-Pflicht aufgerufen, sind die folgenden Phasen zu durchlaufen (Phase 3 nur bei genutztem Refresh).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Anmelden, Erneuern und Abmelden laufen über denselben Endpunkt&#039;&#039;&#039; - den im&lt;br /&gt;
Feld JWT-Endpunkt hinterlegten Pfad. Ein eigener Endpunkt fürs Abmelden ist&lt;br /&gt;
nicht nötig und wäre auch umständlicher: der JWT-Endpunkt greift &#039;&#039;&#039;vor&#039;&#039;&#039; der&lt;br /&gt;
Adressauflösung und braucht deshalb weder eine Zeile in &#039;&#039;RESTSRV_ENDPOINTS&#039;&#039;&lt;br /&gt;
noch eine Berechtigung in &#039;&#039;RESTSRV_ACCESS&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Unterschieden wird nach Verb und Token-Art:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Ablauf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; ohne &#039;&#039;Authorization&#039;&#039; || Anmelden (Skript-Methode &#039;&#039;Authenticate&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; mit Bearer, Refresh-Token || Erneuern (Skript-Methode &#039;&#039;Refresh&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;DELETE&#039;&#039; mit Bearer, Access-Token || Abmelden (kein Skript nötig)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; mit Bearer, Access-Token || &#039;&#039;&#039;401&#039;&#039;&#039; - unverändert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Dass das Abmelden am Verb hängt und nicht allein an der Token-Art, ist&lt;br /&gt;
Absicht. Würde ein vorgelegtes Access-Token beim &#039;&#039;POST&#039;&#039; als Abmeldung gedeutet,&lt;br /&gt;
würde ein Client, der seine beiden Token verwechselt, den Anwender still&lt;br /&gt;
abmelden - statt das klare 401 zu bekommen, an dem der Fehler auffällt.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 1: Token-Bezug===&lt;br /&gt;
&lt;br /&gt;
Der Konsument ruft den JWT-Endpunkt des Zugangs auf, z.B.:&lt;br /&gt;
&lt;br /&gt;
 POST https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
 Body:   {&amp;quot;username&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;password&amp;quot;:&amp;quot;...&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Der Server führt das im Zugang hinterlegte &#039;&#039;&#039;Authentifizierungs-Skript&#039;&#039;&#039; aus (F7 in der Zugangs-Liste). Das Skript muss eine Prozedur &#039;&#039;Authenticate&#039;&#039; bereitstellen, die über den Writer schreibt:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;status&#039;&#039; = &#039;&#039;1&#039;&#039; bei Erfolg, &#039;&#039;9&#039;&#039; bei Misserfolg&lt;br /&gt;
* &#039;&#039;error&#039;&#039; = Fehlertext (bei status=9)&lt;br /&gt;
* &#039;&#039;_OBS_JWT_ID&#039;&#039;, &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039;, &#039;&#039;_OBS_JWT_AUDIENCE&#039;&#039; für die JWT-Claims (optional)&lt;br /&gt;
* &#039;&#039;_OBS_JWT_CLAIM_&amp;amp;lt;name&amp;amp;gt;&#039;&#039; für beliebige Custom-Claims (z.B. Mandant &#039;&#039;tenant&#039;&#039;, Rollen &#039;&#039;roles&#039;&#039;); im Folge-Skript lesbar&lt;br /&gt;
* &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039; (optional) löst die Ausstellung eines Refresh-Tokens aus; &#039;&#039;_OBS_JWT_REFRESH_EXP&#039;&#039; setzt dessen Lebensdauer in Minuten (Default 90 Tage)&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Die Lebensdauer eines Refresh-Tokens ist nach oben auf &#039;&#039;&#039;372 Tage&#039;&#039;&#039;&lt;br /&gt;
begrenzt. Ein mit &#039;&#039;_OBS_JWT_REFRESH_EXP&#039;&#039; länger ausgestelltes Token wird bei der&lt;br /&gt;
Vorlage &#039;&#039;&#039;nicht mehr verifiziert&#039;&#039;&#039; und mit 401 abgewiesen - der Fehler fällt&lt;br /&gt;
also erst Monate später auf, beim ersten Erneuerungsversuch.}}&lt;br /&gt;
&lt;br /&gt;
Die Signatur lautet &#039;&#039;procedure Authenticate(oReader: TxRestReader; oWriter: TxRestWriter);&#039;&#039; - Details unter [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]]. Bei Erfolg signiert der Server einen Token (HS256, Issuer &#039;&#039;OBS REST-Server&#039;&#039;, Lebenszeit gemäß JWT-Exp) und antwortet:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;token&amp;quot;:        &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;accessToken&amp;quot;:  &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;:    28800,&lt;br /&gt;
  &amp;quot;serverTime&amp;quot;:   &amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Zum Aufbau der Antwort:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;token&#039;&#039; und &#039;&#039;accessToken&#039;&#039; enthalten &#039;&#039;&#039;denselben&#039;&#039;&#039; Token. &#039;&#039;accessToken&#039;&#039; ist der Name, den die meisten Client-Bibliotheken erwarten; &#039;&#039;token&#039;&#039; bleibt für bestehende Konsumenten erhalten.&lt;br /&gt;
* &#039;&#039;expiresIn&#039;&#039; ist die Gültigkeit in &#039;&#039;&#039;Sekunden&#039;&#039;&#039; (JWT-Exp × 60).&lt;br /&gt;
* &#039;&#039;&#039;Alle weiteren Felder, die das Authenticate-Skript zurückgibt, werden mit ausgeliefert&#039;&#039;&#039; - mit Ausnahme von &#039;&#039;status&#039;&#039; und den &#039;&#039;_OBS_&#039;&#039;-Steuerfeldern. So kann das Skript z.B. eine Benutzer-Id, den Mandanten oder Rollen direkt in die Anmelde-Antwort legen, ohne dass der Client dafür einen zweiten Aufruf braucht.&lt;br /&gt;
&lt;br /&gt;
Beispiel mit Zusatzfeldern aus dem Skript:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;benutzerId&amp;quot;: &amp;quot;4711&amp;quot;, &amp;quot;tenant&amp;quot;: &amp;quot;nord&amp;quot;, &amp;quot;roles&amp;quot;: [&amp;quot;TECHNIKER&amp;quot;],&lt;br /&gt;
  &amp;quot;token&amp;quot;: &amp;quot;eyJ...&amp;quot;, &amp;quot;accessToken&amp;quot;: &amp;quot;eyJ...&amp;quot;, &amp;quot;refreshToken&amp;quot;: &amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;: 28800, &amp;quot;serverTime&amp;quot;: &amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Mit der Ausstellung legt der Server zu dem Token-Paar eine &#039;&#039;&#039;Sitzungszeile&#039;&#039;&#039; in&lt;br /&gt;
&#039;&#039;RESTSRV_TOKEN&#039;&#039; an. Lässt sich diese Zeile nicht schreiben (Tabelle fehlt,&lt;br /&gt;
Datenbankproblem), wird &#039;&#039;&#039;kein Token ausgeliefert&#039;&#039;&#039;: der Server antwortet mit&lt;br /&gt;
&#039;&#039;&#039;503&#039;&#039;&#039; und &#039;&#039;Retry-After&#039;&#039;. Das ist bewusst von der abgelehnten Anmeldung&lt;br /&gt;
unterschieden - die Zugangsdaten waren in Ordnung, der Versuch darf wiederholt&lt;br /&gt;
werden.&lt;br /&gt;
&lt;br /&gt;
Wird die Anmeldung abgelehnt (&#039;&#039;status&#039;&#039; = 9), antwortet der Server mit&lt;br /&gt;
&#039;&#039;&#039;401 Unauthorized&#039;&#039;&#039;, dem Code &#039;&#039;AUTH_FAILED&#039;&#039; und reicht den Text aus dem Feld&lt;br /&gt;
&#039;&#039;error&#039;&#039; als &#039;&#039;error.message&#039;&#039; an den Client durch - der Konsument kann ihn also&lt;br /&gt;
direkt anzeigen. Die Formulierung liegt damit beim Skript.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;AUTH_FAILED&#039;&#039; unterscheidet die &#039;&#039;&#039;abgelehnte Anmeldung&#039;&#039;&#039; vom&lt;br /&gt;
&#039;&#039;AUTH_EXPIRED&#039;&#039;, das ein abgelaufenes oder gesperrtes Token liefert. Ein Client&lt;br /&gt;
sollte auf den Code reagieren und nicht auf den Text: Bei &#039;&#039;AUTH_FAILED&#039;&#039; gehört&lt;br /&gt;
die Anmeldemaske noch einmal vorgelegt, bei &#039;&#039;AUTH_EXPIRED&#039;&#039; erst ein Refresh&lt;br /&gt;
versucht.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der durchgereichte Text ist für den Endanwender gedacht und sollte&lt;br /&gt;
keine internen Details verraten. „Benutzer oder Passwort ist falsch&amp;quot; ist eine gute&lt;br /&gt;
Meldung, „Kein Datensatz in BENUTZ mit b_name=meier&amp;quot; nicht.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 2: Token-Verwendung===&lt;br /&gt;
&lt;br /&gt;
Bei allen folgenden Aufrufen sendet der Client den Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 GET https://api.meinserver.de/kalender/v1&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi...&lt;br /&gt;
&lt;br /&gt;
Der Server verifiziert Signatur, Issuer, Ausstellzeitpunkt und Ablauf (Toleranz: 20 Sekunden Clock-Skew) und prüft anschliessend die &#039;&#039;&#039;Sitzung&#039;&#039;&#039; in &#039;&#039;RESTSRV_TOKEN&#039;&#039;. Im Skript stehen die Claims als Parameter &#039;&#039;_OBS_JWT_ID&#039;&#039;, &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039; und &#039;&#039;_OBS_JWT_AUDIENCE&#039;&#039; zur Verfügung.&lt;br /&gt;
&lt;br /&gt;
Die Sitzungsprüfung weist ein Token ab, das zu keiner Zeile gehört oder dessen&lt;br /&gt;
Sitzung gesperrt wurde - beides mit &#039;&#039;&#039;401&#039;&#039;&#039; und dem Code &#039;&#039;AUTH_EXPIRED&#039;&#039;. Der&lt;br /&gt;
Grund steht jeweils im Protokoll &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; unter &#039;&#039;TokenStore: ...&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein Token, das &#039;&#039;&#039;vor&#039;&#039;&#039; der Einführung dieser Prüfung ausgestellt&lt;br /&gt;
wurde, gehört zu keiner Sitzung und wird abgewiesen. Nach dem Update müssen sich&lt;br /&gt;
angemeldete Konsumenten also einmalig neu anmelden.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 3: Token erneuern (Refresh)===&lt;br /&gt;
&lt;br /&gt;
Soll die App lange ohne Neuanmeldung arbeiten, stellt das Authenticate-Skript zusätzlich ein Refresh-Token aus (Feld &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039;). Zum Erneuern ruft der Client &#039;&#039;&#039;denselben JWT-Endpunkt&#039;&#039;&#039; auf, aber mit dem Refresh-Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 POST https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi... (Refresh-Token)&lt;br /&gt;
&lt;br /&gt;
Der Server erkennt am Bearer-Token den Refresh-Fall, verifiziert das Token (Signatur, Ablauf, &#039;&#039;token_use=refresh&#039;&#039;), &#039;&#039;&#039;entwertet es in der Sitzung&#039;&#039;&#039; und ruft erst dann im selben Skript die Methode &#039;&#039;&#039;Refresh&#039;&#039;&#039; auf. Das Skript liefert nur noch die aktuellen Claims zurück - Rotation, Einmalgebrauch und Sperrliste erledigt der Server. Die Antwort ist ein rotiertes Paar &#039;&#039;{&amp;quot;token&amp;quot;,&amp;quot;accessToken&amp;quot;,&amp;quot;refreshToken&amp;quot;,&amp;quot;expiresIn&amp;quot;,&amp;quot;serverTime&amp;quot;}&#039;&#039; in &#039;&#039;&#039;derselben Sitzung&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Eine &#039;&#039;&#039;eigene Sperrtabelle im Skript ist nicht mehr nötig&#039;&#039;&#039; und sollte&lt;br /&gt;
entfernt werden. Wer sie weiterführt, rotiert zweimal: einmal im Skript, einmal im&lt;br /&gt;
Server. Der Server sieht die Skript-Tabelle nicht, und das Skript sieht die&lt;br /&gt;
Sitzung nicht.}}&lt;br /&gt;
&lt;br /&gt;
Verhalten bei einem bereits benutzten Refresh-Token:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Lage !! Antwort !! Wirkung auf die Sitzung&lt;br /&gt;
|-&lt;br /&gt;
| Erneute Vorlage &#039;&#039;&#039;innerhalb von 60 Sekunden&#039;&#039;&#039; || 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; || Keine. Das ist der Normalfall einer App, deren parallele Anfragen gleichzeitig in den Refresh laufen - eine gewinnt, die anderen sollen die vom Gewinner gespeicherten Token benutzen&lt;br /&gt;
|-&lt;br /&gt;
| Erneute Vorlage &#039;&#039;&#039;später&#039;&#039;&#039; || 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; || Die &#039;&#039;&#039;gesamte Sitzung wird gesperrt&#039;&#039;&#039;. Das Token wurde kopiert oder wiederholt, ohne dass ein Gewinner es noch brauchen könnte; der Vorfall wird mit IP protokolliert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Details und ein vollständiges Beispiel siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
===Phase 4: Abmelden (Logout)===&lt;br /&gt;
&lt;br /&gt;
Abgemeldet wird mit einem &#039;&#039;&#039;DELETE&#039;&#039;&#039; auf denselben JWT-Endpunkt, das&lt;br /&gt;
Access-Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 DELETE https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi... (Access-Token)&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 204 No Content&lt;br /&gt;
&lt;br /&gt;
Der Server sperrt die &#039;&#039;&#039;ganze Sitzung&#039;&#039;&#039; - also auch die noch nicht&lt;br /&gt;
abgelaufenen Access-Token aller vorherigen Erneuerungen. &#039;&#039;&#039;Ein Skript ist dafür&lt;br /&gt;
nicht nötig&#039;&#039;&#039;: es gibt keine Methode &#039;&#039;Logout&#039;&#039;, der Vorgang läuft komplett in&lt;br /&gt;
der Engine. Damit funktioniert das Abmelden für jeden Zugang mit aktiver&lt;br /&gt;
JWT-Pflicht, ohne dass am Authentifizierungs-Skript etwas geändert wird.&lt;br /&gt;
&lt;br /&gt;
* Ein wiederholter Aufruf ist unschädlich und bleibt &#039;&#039;&#039;204&#039;&#039;&#039; - auch wenn die Sitzung längst gesperrt ist.&lt;br /&gt;
* Ein abgelaufenes oder ungültiges Token ergibt &#039;&#039;&#039;401&#039;&#039;&#039; (es wird schon bei der Prüfung abgewiesen).&lt;br /&gt;
* Ein Refresh-Token statt des Access-Tokens ergibt &#039;&#039;&#039;401&#039;&#039;&#039;.&lt;br /&gt;
* Lässt sich die Sitzung nicht sperren, antwortet der Server &#039;&#039;&#039;503&#039;&#039;&#039; mit &#039;&#039;Retry-After&#039;&#039; - ein Client darf nicht glauben, er sei abgemeldet, während sein Token weiterläuft.&lt;br /&gt;
&lt;br /&gt;
====Alle Geräte abmelden====&lt;br /&gt;
&lt;br /&gt;
Der Fall „Gerät verloren&amp;quot; ist bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; am JWT-Endpunkt untergebracht:&lt;br /&gt;
er braucht eine fachliche Entscheidung darüber, wer das darf. Dafür gibt es das&lt;br /&gt;
&#039;&#039;oWriter.JwtRevoke&#039;&#039;, das jeder gewöhnliche Endpunkt aufrufen kann:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    // &#039;session&#039; = nur diese Sitzung, &#039;all&#039; = alle Sitzungen des Subjects&lt;br /&gt;
    oWriter.JwtRevoke(&#039;all&#039;);&lt;br /&gt;
    oWriter.Status(204);&lt;br /&gt;
    oWriter.NoBody();&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;all&#039;&#039; setzt voraus, dass das Authenticate-Skript &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039;&lt;br /&gt;
gesetzt hat. Ohne Subject lässt sich die Menge der Sitzungen nicht bestimmen, und&lt;br /&gt;
der Aufruf antwortet mit 503.}}&lt;br /&gt;
&lt;br /&gt;
==Woher ein Anmelder seine Identität bekommt==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Diese Frage entscheidet das Skript des Zugangs, nicht der Server.&#039;&#039;&#039; Das&lt;br /&gt;
Authentifizierungs-Skript liegt je Zugang in &amp;lt;code&amp;gt;ra_jwt_script&amp;lt;/code&amp;gt; - und&lt;br /&gt;
damit gilt für jede Anwendung ihre eigene Regel.&lt;br /&gt;
&lt;br /&gt;
Das ist keine Kleinigkeit, sondern der Punkt, an dem sich zwei Anwendungen&lt;br /&gt;
unterscheiden. Ein Beispiel aus dem Betrieb: Ein Zugang für eine&lt;br /&gt;
Techniker-Anwendung lehnt jeden Benutzer ohne Mitarbeiterzuordnung&lt;br /&gt;
(&amp;lt;code&amp;gt;BENUTZ.b_mitarbeiter&amp;lt;/code&amp;gt;) ab - &#039;&#039;&#039;bewusst&#039;&#039;&#039;, denn ein Techniker ohne&lt;br /&gt;
Zuordnung bekäme eine leere Auftragsliste und würde den Fehler bei sich suchen.&lt;br /&gt;
Für einen Kundenzugang wäre genau dieselbe Regel falsch: sie träfe &#039;&#039;&#039;jeden&#039;&#039;&#039;&lt;br /&gt;
Anmelder.&lt;br /&gt;
&lt;br /&gt;
Wer einen zweiten Zugang für eine andere Anwendung anlegt, prüft deshalb als&lt;br /&gt;
Erstes: Woraus besteht die Identität dieser Anmelder, und welche Ablehnung ist&lt;br /&gt;
für sie richtig? Ein kopiertes Skript bringt die Regel der anderen Anwendung&lt;br /&gt;
mit, und sie fällt erst auf, wenn sich der erste echte Anmelder nicht anmelden&lt;br /&gt;
kann.&lt;br /&gt;
&lt;br /&gt;
==Sitzungen (RESTSRV_TOKEN)==&lt;br /&gt;
&lt;br /&gt;
Je ausgestelltem Token-Paar entsteht eine Zeile. Sie enthält die Sitzung, die&lt;br /&gt;
Zeilenkennung, das Subject, die beiden Laufzeiten, IP und Trace-ID der Ausstellung&lt;br /&gt;
sowie die Kennzeichen „verbraucht&amp;quot; und „gesperrt&amp;quot;. Schickt ein Reverse-Proxy den Header&lt;br /&gt;
&#039;&#039;X-Forwarded-For&#039;&#039; mit, steht dessen Wert zusätzlich in &#039;&#039;rt_ip_fwd&#039;&#039;; &#039;&#039;rt_ip&#039;&#039; bleibt&lt;br /&gt;
die IP-Adresse der Verbindung.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Rotation und Widerruf sind getrennt.&#039;&#039;&#039; Ein Refresh entwertet nur das Refresh-Token; der zugehörige Access-Token bleibt bis zu seinem Ablauf gültig. Sonst bekämen parallel laufende Anfragen ein 401, obwohl niemand abgemeldet ist.&lt;br /&gt;
* &#039;&#039;&#039;Die Sitzung überlebt die Rotation.&#039;&#039;&#039; Alle Paare einer Anmeldung tragen dieselbe Sitzungskennung - deshalb kann ein Logout sie gemeinsam sperren.&lt;br /&gt;
* &#039;&#039;&#039;Aufräumen läuft automatisch&#039;&#039;&#039; beim Serverstart und danach höchstens stündlich. Gelöscht wird nur, was nichts mehr entscheiden kann: Zeilen, deren Access- &#039;&#039;&#039;und&#039;&#039;&#039; Refresh-Laufzeit vorbei sind. Eine verbrauchte Zeile bleibt bis dahin stehen, weil sonst die Erkennung eines später vorgelegten gestohlenen Refresh-Tokens verloren ginge.&lt;br /&gt;
* &#039;&#039;&#039;Kein Eingriff nötig.&#039;&#039;&#039; Die Tabelle wird nicht gepflegt; sie ist Diagnose-Material. Alle Abweisungen stehen als &#039;&#039;TokenStore: ...&#039;&#039; in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==mTLS-Subject-Prüfung==&lt;br /&gt;
&lt;br /&gt;
Wird im Feld &#039;&#039;&#039;mTLS-Subject&#039;&#039;&#039; ein Wert hinterlegt, prüft der Server zusätzlich zur Standard-mTLS-Validierung, ob der Subject-DN des (CA-validierten) Client-Zertifikats die hinterlegten &#039;&#039;&#039;RDN-Komponenten&#039;&#039;&#039; enthält. Verglichen wird jede Komponente &#039;&#039;&#039;vollständig&#039;&#039;&#039; (kein Substring-Match) und case-insensitiv; mehrere Komponenten werden durch Komma oder Semikolon getrennt und müssen &#039;&#039;&#039;alle&#039;&#039;&#039; im Subject vorkommen.&lt;br /&gt;
&lt;br /&gt;
Beispiele für Subject-Strings im Cert (OneLine-DN):&lt;br /&gt;
&lt;br /&gt;
 /C=DE/O=Firma GmbH/CN=client-prod.firma.de&lt;br /&gt;
 /C=DE/O=Firma GmbH/CN=client-test.firma.de&lt;br /&gt;
&lt;br /&gt;
Mit &#039;&#039;&#039;CN=client-prod.firma.de&#039;&#039;&#039; im Feld wird genau das prod-Cert akzeptiert, das test-Cert mit 401 abgewiesen. Da jede RDN-Komponente vollständig verglichen wird, matcht z.B. &#039;&#039;&#039;CN=client1&#039;&#039;&#039; nicht fälschlich auch &#039;&#039;&#039;CN=client10&#039;&#039;&#039;. Mehrere Bedingungen lassen sich mit Komma/Semikolon kombinieren, z.B. &#039;&#039;O=Firma GmbH;CN=client-prod.firma.de&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==Listen-Funktionen==&lt;br /&gt;
&lt;br /&gt;
In der Zugänge-Liste (allein):&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfg&#039;&#039;&#039; - neuer Zugang&lt;br /&gt;
* &#039;&#039;&#039;Return&#039;&#039;&#039; - Zugang bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F4&#039;&#039;&#039; - Sortierung&lt;br /&gt;
* &#039;&#039;&#039;F7&#039;&#039;&#039; - JWT-Authentifizierungs-Skript bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F8&#039;&#039;&#039; - Statistik für den Zugang&lt;br /&gt;
&lt;br /&gt;
Aus der Endpunkt-Liste via F6 geöffnet (Auswahl für Berechtigung):&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;F2&#039;&#039;&#039; - markierte Zugänge dem Endpunkt zuordnen&lt;br /&gt;
* &#039;&#039;&#039;F5&#039;&#039;&#039; - Zugang in der Liste markieren / Markierung aufheben&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel3&amp;diff=67070</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Beispiel3</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel3&amp;diff=67070"/>
		<updated>2026-10-26T09:47:30Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Umlaute ausgeschrieben, Beispiel-Domain firma.de statt kunde.de [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Beispiel 3: Datensatz anlegen mit JSON-Body=&lt;br /&gt;
&lt;br /&gt;
Dieses Beispiel zeigt einen Endpunkt mit allen vier CRUD-Zugriffen (GET, POST, PUT und - anstelle von DELETE - PATCH, siehe den Hinweis beim Schliessen). Es geht um Tickets eines externen Servicedesks, der Tickets über den OBS REST-Server in OBS einliefern, abrufen, aktualisieren und schliessen kann.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Dieses Beispiel adressiert ein einzelnes Ticket über einen Query-Parameter (&#039;&#039;?id...&#039;&#039;). Alternativ lässt sich dieselbe Ressource über ein Pfad-Template wie &#039;&#039;/tickets/{id}&#039;&#039; ansprechen; die ID steht dann als &#039;&#039;oReader.Path(&#039;id&#039;)&#039;&#039; bereit. Ein vollständiges Beispiel dazu zeigt [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4 - Pfad-Parameter]].}}&lt;br /&gt;
&lt;br /&gt;
==Einrichtung in OBS==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Server-Profil:&#039;&#039;&#039; Standard-TLS-Profil &#039;&#039;Public-API&#039;&#039;, Bindung 0.0.0.0:443&lt;br /&gt;
* &#039;&#039;&#039;Zugang:&#039;&#039;&#039; &#039;&#039;Servicedesk-X&#039;&#039;&lt;br /&gt;
** API-Key: zufällig generiert&lt;br /&gt;
** Host: &#039;&#039;servicedesk.firma.de&#039;&#039; (DNS-gebunden, IP-Zugriff wird abgelehnt)&lt;br /&gt;
** JWT: nicht aktiv&lt;br /&gt;
* &#039;&#039;&#039;Endpunkt:&#039;&#039;&#039; &#039;&#039;tickets/v1&#039;&#039;, Profil &#039;&#039;Public-API&#039;&#039;&lt;br /&gt;
* &#039;&#039;&#039;Berechtigung:&#039;&#039;&#039; Zugang &#039;&#039;Servicedesk-X&#039;&#039; für Endpunkt &#039;&#039;tickets/v1&#039;&#039; freigeschaltet&lt;br /&gt;
&lt;br /&gt;
==Endpunkt-Skript &#039;&#039;tickets/v1&#039;&#039;==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
// Hilfsprozedur: die FELDER eines Tickets. Sie bekommt den Writer und schreibt&lt;br /&gt;
// in das Objekt, das der Aufrufer geoeffnet hat - einmal als Listeneintrag,&lt;br /&gt;
// einmal in die Wurzel. Ein Helfer, der ein Fragment als String zurueckgibt,&lt;br /&gt;
// waere der alte Weg; dabei ging die Reihenfolge im Zusammenbau verloren.&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure _TicketFelder(oWriter: TxRestWriter; qTicket: TxFQuery;&lt;br /&gt;
                        const cOffset: string);&lt;br /&gt;
begin&lt;br /&gt;
    oWriter.Str (&#039;id&#039;     , qTicket.A2UID());&lt;br /&gt;
    oWriter.Str (&#039;nr&#039;     , qTicket.A2C(&#039;ti_nr&#039;));&lt;br /&gt;
    oWriter.Str (&#039;betreff&#039;, qTicket.A2C(&#039;ti_betreff&#039;));&lt;br /&gt;
    oWriter.Str (&#039;status&#039; , qTicket.A2C(&#039;ti_status&#039;));&lt;br /&gt;
    oWriter.Str (&#039;beschr&#039; , qTicket.A2C(&#039;ti_beschr&#039;));&lt;br /&gt;
    // Zeitpunkte gehen als ISO-8601 MIT OFFSET ueber die Leitung, nicht im&lt;br /&gt;
    // SQL-Format: DTToSQL liefert &#039;JJJJ-MM-TT hh:mm:ss&#039; ohne Zeitzone, und der&lt;br /&gt;
    // Konsument muss dann raten, in welcher er steht. RestIsoFromDate nimmt den&lt;br /&gt;
    // Offset des Servers dazu. Ein leeres Datum ergibt null, nicht 1899.&lt;br /&gt;
    oWriter.StrN(&#039;aenderung&#039;, RestIsoFromDate(qTicket.A2D(&#039;ti_aend_dat&#039;), cOffset));&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
// Fuer den Body-Zugriff und fuer Fehlerantworten braucht es keine eigenen&lt;br /&gt;
// Hilfsfunktionen mehr: oReader liest typisiert, oWriter.Error baut die&lt;br /&gt;
// vollstaendige Fehlerhuelle samt traceId.&lt;br /&gt;
&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
// GET   /tickets/v1?id=...   - einzelnes Ticket&lt;br /&gt;
// GET   /tickets/v1          - alle offenen Tickets&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cSql     : string;&lt;br /&gt;
    qData    : TxFQuery;&lt;br /&gt;
    cId      : string;&lt;br /&gt;
    cOffset  : string;&lt;br /&gt;
    lGefunden: Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    cId     := oReader.Param(&#039;id&#039;);&lt;br /&gt;
    // Der Offset des Servers geht in jeden Zeitstempel der Antwort.&lt;br /&gt;
    cOffset := oReader.Offset();&lt;br /&gt;
&lt;br /&gt;
    if (not Empty(cId)) then begin&lt;br /&gt;
        lGefunden := false;&lt;br /&gt;
        cSql := &#039;SELECT * FROM tickets WHERE sys_uid = &#039; + DB_SQLVal(cId);&lt;br /&gt;
        if (DB_SOpen(oDB, cSql, qData)) then begin&lt;br /&gt;
            // EoF gehoert dazu: DB_SOpen sagt nur, dass die ABFRAGE lief.&lt;br /&gt;
            if (not qData.EoF) then begin&lt;br /&gt;
                _TicketFelder(oWriter, qData, cOffset);&lt;br /&gt;
                lGefunden := true;&lt;br /&gt;
            end;&lt;br /&gt;
        end;&lt;br /&gt;
        DB_Close(qData);&lt;br /&gt;
&lt;br /&gt;
        if (not lGefunden) then begin&lt;br /&gt;
            oWriter.Error(404, &#039;NOT_FOUND&#039;, &#039;Ticket nicht gefunden&#039;);&lt;br /&gt;
        end;&lt;br /&gt;
    end else begin&lt;br /&gt;
        // Die Liste ist eine nackte Liste. RootArr steht vor der Abfrage:&lt;br /&gt;
        // kein Treffer heisst leere Liste, nicht leeres Objekt.&lt;br /&gt;
        oWriter.RootArr();&lt;br /&gt;
        cSql := &#039;SELECT * FROM tickets WHERE ti_status &amp;lt;&amp;gt; &amp;quot;9&amp;quot; ORDER BY ti_aend_dat DESC&#039;;&lt;br /&gt;
        if (DB_SOpen(oDB, cSql, qData)) then begin&lt;br /&gt;
            while (not qData.EoF) do begin&lt;br /&gt;
                oWriter.ObjBegin(&#039;&#039;);&lt;br /&gt;
                    _TicketFelder(oWriter, qData, cOffset);&lt;br /&gt;
                oWriter.ObjEnd;&lt;br /&gt;
                qData.Next();&lt;br /&gt;
            end;&lt;br /&gt;
        end;&lt;br /&gt;
        DB_Close(qData);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
// POST  /tickets/v1          - neues Ticket anlegen&lt;br /&gt;
// Body: { &amp;quot;betreff&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;beschr&amp;quot;:&amp;quot;...&amp;quot; }&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var xTicket : TqSQL;&lt;br /&gt;
    cId     : string;&lt;br /&gt;
    cBetreff: string;&lt;br /&gt;
    cBeschr : string;&lt;br /&gt;
    lOk     : Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    // HasBody unterscheidet &amp;quot;kein Koerper&amp;quot; von &amp;quot;Koerper ohne das Feld&amp;quot; - das&lt;br /&gt;
    // erste ist ein 400, das zweite ein 422.&lt;br /&gt;
    if (not oReader.HasBody()) then begin&lt;br /&gt;
        oWriter.Error(400, &#039;BAD_REQUEST&#039;, &#039;JSON-Body erforderlich&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    cBetreff := &#039;&#039;;&lt;br /&gt;
    cBeschr  := &#039;&#039;;&lt;br /&gt;
    oReader.Str(&#039;betreff&#039;, cBetreff);&lt;br /&gt;
    oReader.Str(&#039;beschr&#039; , cBeschr);&lt;br /&gt;
&lt;br /&gt;
    if (Empty(cBetreff)) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Feld &amp;quot;betreff&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    cId := GetNewId(oDB);&lt;br /&gt;
&lt;br /&gt;
    xTicket := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
    xTicket.lNoSysUID := True;&lt;br /&gt;
    try&lt;br /&gt;
        xTicket.qSet(&#039;sys_uid&#039;    , cId);&lt;br /&gt;
        xTicket.qSet(&#039;ti_nr&#039;      , DB_NeuNum(oDB, &#039;tickets&#039;, &#039;ti_nr&#039;, NEUNUM_HOLE, &#039;&#039;, &#039;&#039;, 1, 999999, &#039;0&#039;));&lt;br /&gt;
        xTicket.qSet(&#039;ti_betreff&#039; , cBetreff);&lt;br /&gt;
        xTicket.qSet(&#039;ti_beschr&#039;  , cBeschr);&lt;br /&gt;
        xTicket.qSet(&#039;ti_status&#039;  , &#039;1&#039;);&lt;br /&gt;
        xTicket.qSet(&#039;ti_anl_dat&#039; , Now());&lt;br /&gt;
        xTicket.qSet(&#039;ti_aend_dat&#039;, Now());&lt;br /&gt;
        // SaveData liefert einen Boolean. Ein SQL-Fehler - etwa Duplicate entry&lt;br /&gt;
        // auf einem eindeutigen Index - wirft KEINE Exception und steht in&lt;br /&gt;
        // keinem Protokoll. Ohne diese Auswertung antwortet der Endpunkt mit&lt;br /&gt;
        // 201, ohne etwas angelegt zu haben.&lt;br /&gt;
        lOk := xTicket.SaveData(NEW_RECORD);&lt;br /&gt;
    finally&lt;br /&gt;
        qSqlFree(xTicket);&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    if (not lOk) then begin&lt;br /&gt;
        oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Ticket konnte nicht angelegt werden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    oWriter.Str(&#039;id&#039;    , cId);&lt;br /&gt;
    oWriter.Str(&#039;status&#039;, &#039;created&#039;);&lt;br /&gt;
    oWriter.Status(201);&lt;br /&gt;
    oWriter.Header(&#039;Location&#039;, &#039;/tickets/v1?id=&#039; + cId);&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
// PUT   /tickets/v1          - Ticket aktualisieren&lt;br /&gt;
// Body: { &amp;quot;id&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;status&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;beschr&amp;quot;:&amp;quot;...&amp;quot; }&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure Put(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var xTicket: TqSQL;&lt;br /&gt;
    lOk    : Boolean;&lt;br /&gt;
    cId    : string;&lt;br /&gt;
    cStat  : string;&lt;br /&gt;
    cBeschr: string;&lt;br /&gt;
begin&lt;br /&gt;
    if (not oReader.HasBody()) then begin&lt;br /&gt;
        oWriter.Error(400, &#039;BAD_REQUEST&#039;, &#039;JSON-Body erforderlich&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    cId     := &#039;&#039;;&lt;br /&gt;
    cStat   := &#039;&#039;;&lt;br /&gt;
    cBeschr := &#039;&#039;;&lt;br /&gt;
    oReader.Str(&#039;id&#039;    , cId);&lt;br /&gt;
    oReader.Str(&#039;status&#039;, cStat);&lt;br /&gt;
    oReader.Str(&#039;beschr&#039;, cBeschr);&lt;br /&gt;
&lt;br /&gt;
    if (Empty(cId)) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Feld &amp;quot;id&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    if (not DB_LSeek(oDB, &#039;tickets&#039;, &#039;sys_uid = &#039; + DB_SQLVal(cId))) then begin&lt;br /&gt;
        oWriter.Error(404, &#039;NOT_FOUND&#039;, &#039;Ticket nicht gefunden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // Zum Aendern qSqlInit mit sys_uid, nicht qSqlRead: qSqlRead liest den&lt;br /&gt;
    // Altsatz und schreibt nur Aenderungen - ein unveraenderter Wert erzeugt&lt;br /&gt;
    // gar kein Write und SaveData liefert False.&lt;br /&gt;
    xTicket := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
    xTicket.qSet(&#039;sys_uid&#039;, cId);&lt;br /&gt;
    try&lt;br /&gt;
        if (not Empty(cStat))   then xTicket.qSet(&#039;ti_status&#039;  , cStat);&lt;br /&gt;
        if (not Empty(cBeschr)) then xTicket.qSet(&#039;ti_beschr&#039;  , cBeschr);&lt;br /&gt;
        xTicket.qSet(&#039;ti_aend_dat&#039;, Now());&lt;br /&gt;
        lOk := xTicket.SaveData(UPDATE_RECORD);&lt;br /&gt;
    finally&lt;br /&gt;
        qSqlFree(xTicket);&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    if (not lOk) then begin&lt;br /&gt;
        oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Ticket konnte nicht geaendert werden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    oWriter.Str(&#039;status&#039;, &#039;updated&#039;);&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
// PATCH /tickets/v1?id=...   - Ticket schliessen (Soft-Delete via Status=9)&lt;br /&gt;
//&lt;br /&gt;
// NICHT als DELETE: &#039;Delete&#039; ist in der Skriptsprache eine Standardprozedur,&lt;br /&gt;
// eine eigene Prozedur dieses Namens laesst sich nicht uebersetzen. Solange das&lt;br /&gt;
// so ist, laeuft ein Loeschen ueber PATCH oder POST. Siehe den Hinweis unten.&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure Patch(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var xTicket: TqSQL;&lt;br /&gt;
    cId    : string;&lt;br /&gt;
    lOk    : Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    cId := oReader.Param(&#039;id&#039;);&lt;br /&gt;
    if (Empty(cId)) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Parameter &amp;quot;id&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    if (not DB_LSeek(oDB, &#039;tickets&#039;, &#039;sys_uid = &#039; + DB_SQLVal(cId))) then begin&lt;br /&gt;
        oWriter.Error(404, &#039;NOT_FOUND&#039;, &#039;Ticket nicht gefunden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    xTicket := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
    xTicket.qSet(&#039;sys_uid&#039;, cId);&lt;br /&gt;
    try&lt;br /&gt;
        xTicket.qSet(&#039;ti_status&#039;  , &#039;9&#039;);&lt;br /&gt;
        xTicket.qSet(&#039;ti_aend_dat&#039;, Now());&lt;br /&gt;
        lOk := xTicket.SaveData(UPDATE_RECORD);&lt;br /&gt;
    finally&lt;br /&gt;
        qSqlFree(xTicket);&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    if (not lOk) then begin&lt;br /&gt;
        oWriter.Error(500, &#039;INTERNAL_ERROR&#039;, &#039;Ticket konnte nicht geschlossen werden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    oWriter.Str(&#039;status&#039;, &#039;closed&#039;);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Ein DELETE-Endpunkt kann derzeit kein Skript haben.&#039;&#039;&#039; Der Server&lt;br /&gt;
ruft die Prozedur, die genauso heisst wie das HTTP-Verb - für DELETE also&lt;br /&gt;
&amp;lt;code&amp;gt;Delete&amp;lt;/code&amp;gt;. Dieser Name ist in der Skriptsprache belegt&lt;br /&gt;
(Standardprozedur zum Löschen aus einer Zeichenkette), und die Deklaration&lt;br /&gt;
lässt sich nicht übersetzen; die Meldung nennt dabei nur die Zeile, nicht die&lt;br /&gt;
Ursache. Bis das gelöst ist: das Löschen als &amp;lt;code&amp;gt;PATCH&amp;lt;/code&amp;gt; oder&lt;br /&gt;
&amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; mit einer Aktion abbilden. Das Abmelden am JWT-Endpunkt ist&lt;br /&gt;
davon nicht betroffen - es läuft als DELETE &#039;&#039;&#039;ohne&#039;&#039;&#039; Skript vollständig in&lt;br /&gt;
der Engine.}}&lt;br /&gt;
&lt;br /&gt;
==Aufruf mit curl==&lt;br /&gt;
&lt;br /&gt;
Liste aller offenen Tickets:&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; https://api.meinserver.de/tickets/v1&lt;br /&gt;
&lt;br /&gt;
Einzelnes Ticket:&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; &amp;quot;https://api.meinserver.de/tickets/v1?id=abc-123&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Neues Ticket anlegen:&lt;br /&gt;
&lt;br /&gt;
 curl -X POST -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Content-Type: application/json&amp;quot; ^&lt;br /&gt;
      -d &amp;quot;{\&amp;quot;betreff\&amp;quot;:\&amp;quot;Drucker offline\&amp;quot;,\&amp;quot;beschr\&amp;quot;:\&amp;quot;Etage 2, Raum 23\&amp;quot;}&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/tickets/v1&lt;br /&gt;
&lt;br /&gt;
Ticket aktualisieren:&lt;br /&gt;
&lt;br /&gt;
 curl -X PUT -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Content-Type: application/json&amp;quot; ^&lt;br /&gt;
      -d &amp;quot;{\&amp;quot;id\&amp;quot;:\&amp;quot;abc-123\&amp;quot;,\&amp;quot;status\&amp;quot;:\&amp;quot;2\&amp;quot;}&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/tickets/v1&lt;br /&gt;
&lt;br /&gt;
Ticket schliessen:&lt;br /&gt;
&lt;br /&gt;
 curl -X PATCH -H &amp;quot;apikey: [API-KEY]&amp;quot; &amp;quot;https://api.meinserver.de/tickets/v1?id=abc-123&amp;quot;&lt;br /&gt;
&lt;br /&gt;
==Aufruf aus PHP==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;php&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
$apikey = &#039;[API-KEY]&#039;;&lt;br /&gt;
$base   = &#039;https://api.meinserver.de/tickets/v1&#039;;&lt;br /&gt;
&lt;br /&gt;
function rest($method, $url, $apikey, $body = null) {&lt;br /&gt;
    $ch = curl_init();&lt;br /&gt;
    curl_setopt($ch, CURLOPT_URL           , $url);&lt;br /&gt;
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);&lt;br /&gt;
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST , $method);&lt;br /&gt;
    $headers = [&#039;apikey: &#039; . $apikey];&lt;br /&gt;
    if ($body !== null) {&lt;br /&gt;
        $headers[] = &#039;Content-Type: application/json&#039;;&lt;br /&gt;
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));&lt;br /&gt;
    }&lt;br /&gt;
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);&lt;br /&gt;
    $res = curl_exec($ch);&lt;br /&gt;
    curl_close($ch);&lt;br /&gt;
    return json_decode($res, true);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Neues Ticket anlegen&lt;br /&gt;
$neu = rest(&#039;POST&#039;, $base, $apikey, [&lt;br /&gt;
    &#039;betreff&#039; =&amp;gt; &#039;Drucker offline&#039;,&lt;br /&gt;
    &#039;beschr&#039;  =&amp;gt; &#039;Etage 2, Raum 23&#039;&lt;br /&gt;
]);&lt;br /&gt;
$id = $neu[&#039;id&#039;];&lt;br /&gt;
&lt;br /&gt;
// Status aktualisieren&lt;br /&gt;
rest(&#039;PUT&#039;, $base, $apikey, [&#039;id&#039; =&amp;gt; $id, &#039;status&#039; =&amp;gt; &#039;2&#039;]);&lt;br /&gt;
&lt;br /&gt;
// Liste auslesen&lt;br /&gt;
$alle = rest(&#039;GET&#039;, $base, $apikey);&lt;br /&gt;
foreach ($alle as $t) {&lt;br /&gt;
    echo $t[&#039;nr&#039;] . &#039; - &#039; . $t[&#039;betreff&#039;] . PHP_EOL;&lt;br /&gt;
}&lt;br /&gt;
?&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Was zeigt das Beispiel?==&lt;br /&gt;
&lt;br /&gt;
* Saubere Trennung der CRUD-Methoden in einem Endpunkt.&lt;br /&gt;
* Verwendung des JSON-Body für komplexere Eingangsdaten (POST, PUT).&lt;br /&gt;
* Verwendung von Query-Parametern für einfache Werte (GET, PATCH).&lt;br /&gt;
* Soft-Delete über Status-Änderung statt physischer Löschung - und warum er als PATCH und nicht als DELETE läuft.&lt;br /&gt;
* Eindeutige Audit-UIDs (&#039;&#039;Y00CXXXXxx&#039;&#039;) für jeden DB-Zugriff zur Nachvollziehbarkeit.&lt;br /&gt;
* Host-gebundener Zugang als zweite Sicherheitsebene neben dem API-Key.&lt;br /&gt;
* Alternativ: ID per Pfad-Parameter statt Query-Parameter (siehe [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4]]).&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel1&amp;diff=67069</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Beispiel1</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Beispiel1&amp;diff=67069"/>
		<updated>2026-10-26T09:47:29Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Umlaute ausgeschrieben, Beispiel-Domain firma.de statt kunde.de [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Beispiel 1: Daten-Abruf mit JWT-Authentifizierung=&lt;br /&gt;
&lt;br /&gt;
In diesem Beispiel werden Steuerungs-Variablen aus OBS abgerufen und auf einer Webseite ausgegeben. Der Zugang ist mit JWT-Pflicht eingerichtet, der Konsument muss sich daher zuerst anmelden und einen Token bezogen haben.&lt;br /&gt;
&lt;br /&gt;
==Einrichtung in OBS==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Server-Profil:&#039;&#039;&#039; Standard-TLS-Profil &#039;&#039;Public-API&#039;&#039; mit Bindung 0.0.0.0:443&lt;br /&gt;
* &#039;&#039;&#039;Zugang:&#039;&#039;&#039; &#039;&#039;Web-Dashboard&#039;&#039;&lt;br /&gt;
** API-Key: zufällig generiert&lt;br /&gt;
** JWT aktiv, JWT-Endpunkt &#039;&#039;oauth&#039;&#039;, JWT-Key zufällig, JWT-Exp 60 (Minuten)&lt;br /&gt;
** CORS-Origins: &#039;&#039;https://dashboard.firma.de&#039;&#039;&lt;br /&gt;
* &#039;&#039;&#039;Endpunkt:&#039;&#039;&#039; &#039;&#039;steuerung/v1&#039;&#039;, dem Profil &#039;&#039;Public-API&#039;&#039; zugeordnet&lt;br /&gt;
* &#039;&#039;&#039;Berechtigung:&#039;&#039;&#039; Zugang &#039;&#039;Web-Dashboard&#039;&#039; für Endpunkt &#039;&#039;steuerung/v1&#039;&#039; freigeschaltet&lt;br /&gt;
&lt;br /&gt;
==JWT-Authentifizierungs-Skript (Zugang)==&lt;br /&gt;
&lt;br /&gt;
Wird über die Zugänge-Liste mit &#039;&#039;&#039;F7&#039;&#039;&#039; geöffnet. Prüfen, ob Benutzername/Passwort gegen die OBS-Benutzerverwaltung passen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Authenticate(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUser : string;&lt;br /&gt;
    cPass : string;&lt;br /&gt;
    cSql  : string;&lt;br /&gt;
    qUser : TxFQuery;&lt;br /&gt;
    lOk   : Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    cUser := &#039;&#039;;&lt;br /&gt;
    cPass := &#039;&#039;;&lt;br /&gt;
    oReader.Str(&#039;username&#039;, cUser);&lt;br /&gt;
    oReader.Str(&#039;password&#039;, cPass);&lt;br /&gt;
&lt;br /&gt;
    cSql := &#039;SELECT u_nr, u_name FROM benutzer&#039; +&lt;br /&gt;
            &#039; WHERE u_login = &#039; + DB_SQLVal(cUser) +&lt;br /&gt;
            &#039; AND u_passwort_hash = &#039; + DB_SQLVal(HashPasswort(cPass)) +&lt;br /&gt;
            &#039; AND u_aktiv = &#039; + DB_SQLVal(&#039;1&#039;);&lt;br /&gt;
&lt;br /&gt;
    // DB_SOpen liefert true, wenn die ABFRAGE lief - nicht, wenn ein&lt;br /&gt;
    // Datensatz gefunden wurde. Ohne die EoF-Pruefung wuerde jede&lt;br /&gt;
    // Anmeldung gelingen, sobald das SQL fehlerfrei ist.&lt;br /&gt;
    lOk := false;&lt;br /&gt;
    if (DB_SOpen(oDB, cSql, qUser)) then begin&lt;br /&gt;
        if (not qUser.EoF) then begin&lt;br /&gt;
            lOk := true;&lt;br /&gt;
            oWriter.Int(&#039;status&#039;           , 1);&lt;br /&gt;
            oWriter.Str(&#039;_OBS_JWT_ID&#039;      , qUser.A2C(&#039;u_nr&#039;));&lt;br /&gt;
            oWriter.Str(&#039;_OBS_JWT_SUBJECT&#039; , qUser.A2C(&#039;u_name&#039;));&lt;br /&gt;
            oWriter.Str(&#039;_OBS_JWT_AUDIENCE&#039;, &#039;dashboard&#039;);&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
    DB_Close(qUser);&lt;br /&gt;
&lt;br /&gt;
    if (not lOk) then begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;, 9);&lt;br /&gt;
        oWriter.Str(&#039;error&#039; , &#039;Benutzer oder Passwort ist falsch&#039;);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Hier steht in &#039;&#039;_OBS_JWT_ID&#039;&#039; die Benutzernummer - derselbe Wert also bei&lt;br /&gt;
jeder Anmeldung desselben Benutzers. Das ist zulässig: der Server führt seine&lt;br /&gt;
Sitzungen über eigene Kennungen (&#039;&#039;sid&#039;&#039;/&#039;&#039;rid&#039;&#039;), nicht über das &#039;&#039;jti&#039;&#039; des&lt;br /&gt;
Skripts. Mehrfache und parallele Anmeldungen eines Benutzers sind damit&lt;br /&gt;
unproblematisch.}}&lt;br /&gt;
&lt;br /&gt;
==Endpunkt-Skript &#039;&#039;steuerung/v1&#039;&#039;==&lt;br /&gt;
&lt;br /&gt;
Liefert eine Liste von Steuerungs-Werten zurück. Die Auswertung der JWT-Claims sorgt dafür, dass nur Audience &#039;&#039;dashboard&#039;&#039; Daten erhält.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
// Ein Wert als Listeneintrag. Der Helfer bekommt den WRITER und schreibt an&lt;br /&gt;
// der Stelle hinein, an der er aufgerufen wird - er gibt kein Fragment zurueck.&lt;br /&gt;
// Der leere Feldname ist der Eintrag einer Liste.&lt;br /&gt;
//------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
procedure _AddVar(oWriter: TxRestWriter; const cVar: string);&lt;br /&gt;
var cTitel : string;&lt;br /&gt;
    cData  : string;&lt;br /&gt;
    cEinh  : string;&lt;br /&gt;
    lString: Boolean;&lt;br /&gt;
    lAlign : Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    if (ST_Variable(oDB, cVar, cTitel, cData, cEinh, lString, lAlign)) then begin&lt;br /&gt;
        oWriter.ObjBegin(&#039;&#039;);&lt;br /&gt;
            oWriter.Str(&#039;variable&#039;, cVar);&lt;br /&gt;
            oWriter.Str(&#039;titel&#039;   , cTitel);&lt;br /&gt;
            oWriter.Str(&#039;wert&#039;    , cData);&lt;br /&gt;
            oWriter.Str(&#039;einheit&#039; , cEinh);&lt;br /&gt;
        oWriter.ObjEnd;&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    // Audience-Pruefung: nur Dashboard-Tokens akzeptieren. Die Ablehnung ist&lt;br /&gt;
    // ein 403 und keine 200 mit Fehlertext - oWriter.Error baut dieselbe&lt;br /&gt;
    // Huelle, die auch die Server-eigenen Fehler tragen.&lt;br /&gt;
    if (oReader.Param(&#039;_OBS_JWT_AUDIENCE&#039;) &amp;lt;&amp;gt; &#039;dashboard&#039;) then begin&lt;br /&gt;
        oWriter.Error(403, &#039;FORBIDDEN_ROLE&#039;, &#039;Token nicht für diese Anwendung ausgestellt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // Die Antwort ist eine nackte Liste, kein Objekt: RootArr sagt das, und&lt;br /&gt;
    // zwar VOR dem ersten Eintrag. Ein ArrEnd gibt es dazu nicht - die&lt;br /&gt;
    // Wurzelklammer setzt der Server.&lt;br /&gt;
    oWriter.RootArr();&lt;br /&gt;
    _AddVar(oWriter, &#039;WERT_1&#039;);&lt;br /&gt;
    _AddVar(oWriter, &#039;WERT_2&#039;);&lt;br /&gt;
    _AddVar(oWriter, &#039;WERT_3&#039;);&lt;br /&gt;
    _AddVar(oWriter, &#039;WERT_4&#039;);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==JavaScript-Client (Browser)==&lt;br /&gt;
&lt;br /&gt;
Im Browser werden zwei Aufrufe gemacht: erst Token holen, dann Daten lesen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;javascript&amp;quot; line&amp;gt;&lt;br /&gt;
const API_BASE = &#039;https://api.meinserver.de&#039;;&lt;br /&gt;
const API_KEY  = &#039;[API-KEY]&#039;;&lt;br /&gt;
&lt;br /&gt;
async function login(username, password) {&lt;br /&gt;
    const res = await fetch(`${API_BASE}/oauth/`, {&lt;br /&gt;
        method:  &#039;POST&#039;,&lt;br /&gt;
        headers: { &#039;Content-Type&#039;: &#039;application/json&#039;, &#039;apikey&#039;: API_KEY },&lt;br /&gt;
        body:    JSON.stringify({ username, password })&lt;br /&gt;
    });&lt;br /&gt;
    if (!res.ok) throw new Error(&#039;Login fehlgeschlagen&#039;);&lt;br /&gt;
    const data = await res.json();&lt;br /&gt;
    return data.token;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function ladeSteuerung(token) {&lt;br /&gt;
    const res = await fetch(`${API_BASE}/steuerung/v1`, {&lt;br /&gt;
        method:  &#039;GET&#039;,&lt;br /&gt;
        headers: {&lt;br /&gt;
            &#039;apikey&#039;:        API_KEY,&lt;br /&gt;
            &#039;Authorization&#039;: &#039;Bearer &#039; + token&lt;br /&gt;
        }&lt;br /&gt;
    });&lt;br /&gt;
    if (!res.ok) throw new Error(&#039;Abruf fehlgeschlagen: &#039; + res.status);&lt;br /&gt;
    return res.json();&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function zeigeTabelle() {&lt;br /&gt;
    const token = await login(&#039;mitarbeiter&#039;, &#039;geheim&#039;);&lt;br /&gt;
    const werte = await ladeSteuerung(token);&lt;br /&gt;
&lt;br /&gt;
    const tbl = document.getElementById(&#039;steuerung&#039;);&lt;br /&gt;
    werte.forEach(w =&amp;gt; {&lt;br /&gt;
        const tr = document.createElement(&#039;tr&#039;);&lt;br /&gt;
        tr.innerHTML = `&amp;lt;td&amp;gt;${w.titel}&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;${w.wert} ${w.einheit}&amp;lt;/td&amp;gt;`;&lt;br /&gt;
        tbl.appendChild(tr);&lt;br /&gt;
    });&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
zeigeTabelle();&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Test mit curl==&lt;br /&gt;
&lt;br /&gt;
Token holen:&lt;br /&gt;
&lt;br /&gt;
 curl -X POST -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Content-Type: application/json&amp;quot; ^&lt;br /&gt;
      -d &amp;quot;{\&amp;quot;username\&amp;quot;:\&amp;quot;mitarbeiter\&amp;quot;,\&amp;quot;password\&amp;quot;:\&amp;quot;geheim\&amp;quot;}&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/oauth/&lt;br /&gt;
&lt;br /&gt;
Antwort:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;token&amp;quot;:       &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;accessToken&amp;quot;: &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;:   3600,&lt;br /&gt;
  &amp;quot;serverTime&amp;quot;:  &amp;quot;2026-08-19T09:12:33+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Da das Skript kein &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039; liefert, enthält die Antwort kein&lt;br /&gt;
&#039;&#039;refreshToken&#039;&#039; - der Client meldet sich nach Ablauf der 60 Minuten neu an.&lt;br /&gt;
&lt;br /&gt;
Daten abrufen:&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; -H &amp;quot;Authorization: Bearer eyJhbGciOi...&amp;quot; ^&lt;br /&gt;
      https://api.meinserver.de/steuerung/v1&lt;br /&gt;
&lt;br /&gt;
==Was zeigt das Beispiel?==&lt;br /&gt;
&lt;br /&gt;
* Zweistufige Anmeldung: API-Key + JWT.&lt;br /&gt;
* Sichere Trennung von Konsumenten-Gruppen über die Audience-Claim.&lt;br /&gt;
* Eigenes Authentifizierungs-Skript pro Zugang.&lt;br /&gt;
* Browser-Zugriff über CORS-erlaubtes Origin.&lt;br /&gt;
* Anmeldung ohne Refresh-Token: einfachster Fall, der Token läuft nach der Zeit aus &#039;&#039;JWT-Exp&#039;&#039; ab.&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/%C3%96ffentliche_Freigabe&amp;diff=67067</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/%C3%96ffentliche_Freigabe&amp;diff=67067"/>
		<updated>2026-10-26T09:41:07Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Abschnitt Schutz durch den REST-Server ergänzt, Portfreigabe: Vor- und Nachteil präzisiert [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
{{Seitentyp|Übersicht}}&lt;br /&gt;
= Öffentliche Freigabe =&lt;br /&gt;
&lt;br /&gt;
Apps auf mobilen Geräten und externe Systeme erreichen den REST-Server nur, wenn er aus dem Internet erreichbar ist. Diese Seite stellt die drei Wege dafür gegenüber und richtet sich an die IT-Administration, die über die Freigabe entscheidet.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Läuft der REST-Server auf dem OBS-Server, liegt dort auch die Datenbank. Die Varianten unterscheiden sich vor allem darin, ob eine Verbindung aus dem Internet diesen Server direkt erreicht.}}&lt;br /&gt;
&lt;br /&gt;
== Varianten im Vergleich ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Merkmal !! Direkte Portfreigabe !! Reverse-Proxy !! VPN&lt;br /&gt;
|-&lt;br /&gt;
| Aus dem Internet erreichbar || OBS-Server: Port des REST-Servers, bei Prüfart http-01 zusätzlich Port 80 || nur der Proxy (Port 443) || nur das VPN-Gateway&lt;br /&gt;
|-&lt;br /&gt;
| Aufwand für die Nutzer der App || keiner || keiner || VPN-Client einrichten und vor der Nutzung einschalten&lt;br /&gt;
|-&lt;br /&gt;
| Aufwand für die IT || Portweiterleitung am Router || Proxy betreiben, Zertifikat am Proxy || VPN-Zugang für jedes Gerät&lt;br /&gt;
|-&lt;br /&gt;
| Filtern und Protokollieren vor dem OBS-Server || – || ✓ || nur auf Netzebene&lt;br /&gt;
|-&lt;br /&gt;
| Öffentliches Zertifikat || im REST-Server, Prüfart http-01 oder dns-01 || am Proxy || im REST-Server, Prüfart dns-01&lt;br /&gt;
|-&lt;br /&gt;
| REST-Server sieht die IP-Adresse des Geräts || ✓ || nur im Protokoll (über &#039;&#039;X-Forwarded-For&#039;&#039;) || ✓, sofern das VPN-Gateway die Adressen nicht umsetzt (NAT)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Schutz durch den REST-Server ==&lt;br /&gt;
Unabhängig von der Variante schützt sich der REST-Server selbst. Diese Mechanismen greifen in allen drei Varianten:&lt;br /&gt;
* &#039;&#039;&#039;Verschlüsselung:&#039;&#039;&#039; ausschliesslich TLS 1.2 mit Forward Secrecy und AEAD-Verfahren. Unverschlüsselte Anfragen an einen TLS-Port werden vor jeder Verarbeitung abgewiesen. Optional mTLS: jeder Client braucht ein gültiges Client-Zertifikat.&lt;br /&gt;
* &#039;&#039;&#039;Zugänge:&#039;&#039;&#039; Jede Anfrage braucht einen gültigen API-Key, wahlweise zusätzlich JWT. Jeder Zugang ist nur für die Endpunkte freigeschaltet, die er braucht, und lässt sich auf eine Absenderadresse beschränken. API-Keys lassen sich ohne Unterbrechung tauschen.&lt;br /&gt;
* &#039;&#039;&#039;Widerrufbare Sitzungen:&#039;&#039;&#039; Eine Abmeldung wirkt sofort. Ein Refresh-Token gilt genau einmal; wird ein verbrauchtes erneut vorgelegt, sperrt der Server die ganze Sitzung.&lt;br /&gt;
* &#039;&#039;&#039;Grenzen gegen Missbrauch:&#039;&#039;&#039; getrenntes Rate-Limit für erfolgreiche und fehlgeschlagene Anfragen, eine Obergrenze gleichzeitiger Anfragen und Verbindungen sowie Grössengrenzen für Anfragen und Uploads.&lt;br /&gt;
* &#039;&#039;&#039;Keine Informationen nach aussen:&#039;&#039;&#039; Fehlerantworten enthalten nie interne Details. Die stehen nur im Protokoll, mit einer Korrelations-ID je Anfrage. Kennwörter und Token werden dort maskiert.&lt;br /&gt;
* &#039;&#039;&#039;Kurze Öffnung für die Zertifikatsprüfung:&#039;&#039;&#039; Bei http-01 öffnet der Dienst Port 80 nur für die Dauer der Prüfung.&lt;br /&gt;
&lt;br /&gt;
Diese Mechanismen schützen den REST-Dienst. Reverse-Proxy und VPN fügen eine Schicht davor hinzu: Sie halten den Server, auf dem auch die Datenbank läuft, aus dem Internet heraus. Das wirkt auch gegen Schwachstellen ausserhalb des REST-Servers und gegen Überlastung.&lt;br /&gt;
Der Router leitet den Port des REST-Servers aus dem Internet auf den OBS-Server weiter. Für die Zertifikatserneuerung mit der Prüfart http-01 wird zusätzlich Port 80 weitergeleitet.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Die einfachste Einrichtung, es wird keine weitere Komponente gebraucht.&lt;br /&gt;
* Die App funktioniert überall, ohne dass die Nutzer etwas tun müssen.&lt;br /&gt;
* Der REST-Server sieht die IP-Adresse jedes Geräts. Alle Schutzmechanismen des REST-Servers, auch Host-Beschränkung und Rate-Limit, arbeiten damit je Gerät.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Der OBS-Server mit der Datenbank ist direkt aus dem Internet erreichbar. Die [[#Schutz durch den REST-Server|Schutzmechanismen des REST-Servers]] greifen, eine zusätzliche Schicht davor gibt es aber nicht.&lt;br /&gt;
* Mit der Prüfart http-01 ist zusätzlich Port 80 weitergeleitet. Mit der Prüfart dns-01 entfällt das.&lt;br /&gt;
&lt;br /&gt;
== Reverse-Proxy ==&lt;br /&gt;
Ein Reverse-Proxy, zum Beispiel in einer DMZ oder als Funktion der Firewall, nimmt HTTPS auf Port 443 an. Er prüft die Anfrage und reicht sie über eine eigene, ebenfalls verschlüsselte Verbindung an den REST-Server weiter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Der OBS-Server ist aus dem Internet nicht erreichbar, nur der Proxy.&lt;br /&gt;
* Am Proxy lassen sich Anfragen nach Ihren Vorgaben filtern, drosseln und protokollieren.&lt;br /&gt;
* Für die Nutzer der App ändert sich nichts.&lt;br /&gt;
* Das öffentliche Zertifikat liegt am Proxy. Die Zertifikatserneuerung des REST-Servers wird nicht gebraucht.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Der Proxy muss betrieben und gepflegt werden.&lt;br /&gt;
* Der REST-Server sieht als Absender nur den Proxy. Die Host-Beschränkung eines Zugangs kann keine Geräte unterscheiden, und Anfragen ohne API-Key teilen sich einen Zähler des Rate-Limits. Die IP-Adresse des Geräts steht nur im Protokoll, wenn der Proxy den Header &#039;&#039;X-Forwarded-For&#039;&#039; setzt.&lt;br /&gt;
&lt;br /&gt;
Die Einrichtung beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung#Betrieb hinter einem Reverse-Proxy|Betrieb hinter einem Reverse-Proxy]].&lt;br /&gt;
&lt;br /&gt;
== VPN ==&lt;br /&gt;
Die Geräte verbinden sich per VPN mit dem Netz, in dem der OBS-Server steht. Die App spricht durch den Tunnel per HTTPS mit dem REST-Server. Aus dem Internet ist nur das VPN-Gateway erreichbar.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Für den REST-Server ist kein Port aus dem Internet offen.&lt;br /&gt;
* Nur Geräte mit VPN-Zugang erreichen den REST-Server.&lt;br /&gt;
* Setzt das VPN-Gateway die Adressen nicht um, sieht der REST-Server die VPN-Adresse jedes Geräts.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Jedes Gerät braucht einen VPN-Client und einen eigenen Zugang. Ohne zentrale Geräteverwaltung schalten die Nutzer das VPN von Hand ein. Ist es aus, meldet die App, dass der Server nicht erreichbar ist.&lt;br /&gt;
* Ein VPN-Zugang öffnet den Weg ins Netz. Beschränken Sie ihn auf die Adresse und den Port des REST-Servers.&lt;br /&gt;
* Das Zertifikat lässt sich nur mit der Prüfart dns-01 erneuern, weil die Zertifizierungsstelle den Server nicht erreicht.&lt;br /&gt;
* Der Name des REST-Servers muss auf dem Gerät auf die interne Adresse zeigen. Das geht über einen DNS-Server, den das VPN mitliefert, oder über einen öffentlichen DNS-Eintrag mit der internen Adresse. Manche Router verwerfen solche Antworten als Schutz gegen DNS-Rebinding; dann löst der Name in diesem Netz nicht auf.&lt;br /&gt;
&lt;br /&gt;
Das Zertifikat richten Sie wie unter [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]] beschrieben mit der Prüfart dns-01 ein.&lt;br /&gt;
&lt;br /&gt;
== Entscheidungshilfe ==&lt;br /&gt;
* &#039;&#039;&#039;Keine eingehende Verbindung zum OBS-Server erlaubt, DMZ oder Firewall mit Proxy-Funktion vorhanden:&#039;&#039;&#039; Reverse-Proxy.&lt;br /&gt;
* &#039;&#039;&#039;Geräte werden zentral verwaltet oder ein VPN ist ohnehin im Einsatz:&#039;&#039;&#039; VPN.&lt;br /&gt;
* &#039;&#039;&#039;Kleine Installation ohne Vorgaben der IT:&#039;&#039;&#039; Direkte Portfreigabe, möglichst mit der Prüfart dns-01, damit Port 80 geschlossen bleibt.&lt;br /&gt;
&lt;br /&gt;
== Siehe auch ==&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge#Host-Beschränkung|Host-Beschränkung]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer&amp;diff=67064</id>
		<title>OBS/Kostenpflichtige Module/RESTServer</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer&amp;diff=67064"/>
		<updated>2026-10-26T09:31:38Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Weiterführende Seiten: Öffentliche Freigabe und Zertifikatserneuerung ergänzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
=REST-Server=&lt;br /&gt;
{{Hinweis|Es handelt sich um ein kostenpflichtiges Modul. Die Schnittstelle muss über den OBS-Support aktiviert werden.}}&lt;br /&gt;
&lt;br /&gt;
==Was leistet der REST-Server?==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server ist die &#039;&#039;&#039;universelle Schnittstelle&#039;&#039;&#039; Ihrer OBS-Installation nach außen. Er macht aus Ihrem OBS einen Dienst, mit dem andere Programme - Webseiten, Apps, Geräte, Kundensysteme, Cloud-Dienste - direkt sprechen können. Statt Daten manuell zu exportieren, zu mailen oder über Umwege bereitzustellen, holen sich angebundene Systeme genau die Information, die sie brauchen, in dem Moment, in dem sie sie brauchen - oder liefern neue Daten direkt in OBS ab.&lt;br /&gt;
&lt;br /&gt;
Für Anwender, die nicht selbst programmieren, bedeutet das: &#039;&#039;&#039;Was bisher nur manuell, per Datei-Import oder über Spezialschnittstellen ging, lässt sich jetzt automatisieren.&#039;&#039;&#039; Eine Webseite zeigt live aktuelle Lagerbestände. Mitarbeiter erfassen Zeiten über das Handy, ohne dass jemand Excel-Listen einpflegen muss. Ein Kunde stößt per Bestelltaste in seinem System einen Vorgang in Ihrem OBS an. Ein Lieferant meldet Wareneingänge automatisch zurück. Jeder dieser Anwendungsfälle wird einmal eingerichtet und läuft danach automatisch.&lt;br /&gt;
&lt;br /&gt;
Technisch gesehen ist der REST-Server ein in OBS integrierter, voll konfigurierbarer &#039;&#039;&#039;HTTP/HTTPS-Dienst&#039;&#039;&#039;, dessen Endpunkte über in OBS gepflegte Pascal-Skripte realisiert werden. Jeder Endpunkt bekommt eine eigene Adresse, eine eigene Logik und eine eigene Berechtigungsstruktur. Rückgaben erfolgen ausschliesslich im JSON-Format. Damit lassen sich beliebige Lese- und Schreibvorgänge auf der OBS-Datenbank realisieren, ohne dass externe Systeme direkten Datenbank-Zugriff erhalten - das ganze OBS-Regelwerk (Rechte, Validierung, Geschäftslogik) bleibt aktiv.&lt;br /&gt;
&lt;br /&gt;
Im Ergebnis ist der REST-Server kein einzelnes Feature, sondern ein &#039;&#039;&#039;Werkzeugkasten&#039;&#039;&#039;: Was an Daten oder Funktionen in OBS verfügbar ist, kann über den REST-Server auch nach außen angeboten - oder von außen entgegengenommen - werden. Die Bandbreite reicht vom einfachen Lese-Endpunkt für ein Webseiten-Widget bis hin zu kompletten B2B-Integrationen mit Mandanten- und Rollen-Trennung.&lt;br /&gt;
&lt;br /&gt;
Aufruf des Moduls in OBS:&lt;br /&gt;
&#039;&#039;&#039;Stammdaten&#039;&#039;&#039; -&amp;gt; &#039;&#039;&#039;Z Weitere Stammdaten&#039;&#039;&#039; -&amp;gt; &#039;&#039;&#039;REST-Server&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Anwendungsbereiche==&lt;br /&gt;
&lt;br /&gt;
Die folgenden Einsatzfälle sind typische Beispiele, an denen sich der praktische Nutzen ablesen lässt. Es ist keine abschliessende Liste - alles, was sich in OBS-Skripten abbilden lässt, kann auch über einen Endpunkt bereitgestellt werden.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Dies sind Beispiele. Für die konkrete Entwicklung können, je nach Komplexität, weitere Kosten anfallen.}}&lt;br /&gt;
&lt;br /&gt;
===Webseiten und Kundenportale===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Live-Daten auf der eigenen Webseite:&#039;&#039;&#039; Lagerbestände, Verfügbarkeiten, Preise, Auftragsstatus aus OBS direkt anzeigen, ohne tägliche Datei-Exports.&lt;br /&gt;
* &#039;&#039;&#039;Kundenportale:&#039;&#039;&#039; Kunden sehen ihre offenen Posten, Auftragshistorie, Lieferscheine. Die Seite holt sich die Daten zur Anzeigezeit direkt aus OBS, jeder Kunde sieht nur seine eigenen Daten.&lt;br /&gt;
* &#039;&#039;&#039;Trackingseiten:&#039;&#039;&#039; Status einer Bestellung, eines Auftrags oder einer Reparatur für Endkunden, abrufbar über eine Sendungsnummer.&lt;br /&gt;
&lt;br /&gt;
===Mobile Anwendungen und Außendienst===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Mobile Zeiterfassung:&#039;&#039;&#039; Mitarbeiter buchen Anfang, Pause und Ende über Smartphone oder Tablet, die Daten landen direkt in OBS - ohne Zettelwirtschaft.&lt;br /&gt;
* &#039;&#039;&#039;QR-/Barcode-Scanner und Lagergeräte:&#039;&#039;&#039; Wareneingang, Inventur, Kommissionierung direkt in OBS abbilden, ohne zwischengeschaltete Software.&lt;br /&gt;
* &#039;&#039;&#039;Außendienst-Apps:&#039;&#039;&#039; Techniker sehen ihre Aufträge, dokumentieren Einsätze, erfassen Material - synchronisiert mit OBS.&lt;br /&gt;
&lt;br /&gt;
===Anbindung von Drittsystemen===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Webshops:&#039;&#039;&#039; Bestellungen werden direkt vom Shop in OBS gemeldet, Lagerbestände und Preise vom Shop bei OBS abgefragt.&lt;br /&gt;
* &#039;&#039;&#039;Buchhaltungs- und ERP-Systeme:&#039;&#039;&#039; Bidirektionaler Austausch von Belegen, Stammdaten und Buchungen.&lt;br /&gt;
* &#039;&#039;&#039;CRM- und Marketing-Tools:&#039;&#039;&#039; Personendaten, Aktivitäten oder Mailing-Empfänger werden synchron gehalten.&lt;br /&gt;
* &#039;&#039;&#039;Logistikdienstleister:&#039;&#039;&#039; Versandaufträge werden übergeben, Tracking-Informationen zurückgeliefert.&lt;br /&gt;
&lt;br /&gt;
===Automatisierte Rückmeldungen (WebHooks)===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Zahlungs-Callbacks:&#039;&#039;&#039; Bezahldienste (z.B. PayPal, Klarna, Stripe) melden den Zahlungseingang an einen WebHook-Endpunkt - OBS bucht automatisch.&lt;br /&gt;
* &#039;&#039;&#039;Versanddienste:&#039;&#039;&#039; Statusmeldungen (verschickt, zugestellt, retour) werden direkt im passenden Vorgang dokumentiert.&lt;br /&gt;
* &#039;&#039;&#039;Cloud-Workflows:&#039;&#039;&#039; Externe Automatisierungs-Plattformen (Make, Zapier, n8n, ...) stoßen OBS-Vorgänge an oder werden von OBS aus angesprochen.&lt;br /&gt;
&lt;br /&gt;
===Geschäftspartner-Kommunikation (B2B)===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lieferanten-Schnittstelle:&#039;&#039;&#039; Bestellungen werden automatisch übergeben, Auftragsbestätigungen und Lieferavise zurückgespielt.&lt;br /&gt;
* &#039;&#039;&#039;Kunden-Schnittstelle:&#039;&#039;&#039; Grosskunden bestellen direkt aus ihrem ERP heraus, Stammdaten und Konditionen werden zentral gepflegt.&lt;br /&gt;
* &#039;&#039;&#039;Sichere Maschine-zu-Maschine-Kommunikation:&#039;&#039;&#039; Mit Mutual-TLS (mTLS) wird sichergestellt, dass nur die freigeschalteten Partnersysteme - und niemand sonst - mit OBS sprechen können.&lt;br /&gt;
&lt;br /&gt;
===Interne Microservices und Automatisierungen===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Eigene kleine Hilfs-Tools:&#039;&#039;&#039; Zeitschaltautomatik, Reporting-Generator, Sammelaktionen, die aus mehreren Quellen Daten in OBS verarbeiten.&lt;br /&gt;
* &#039;&#039;&#039;Verbindung mehrerer Standorte:&#039;&#039;&#039; Verteilte Systeme tauschen Daten über definierte Endpunkte aus, ohne offene Datenbankverbindungen.&lt;br /&gt;
* &#039;&#039;&#039;Datenbereitstellung für Dashboards und Auswertungen:&#039;&#039;&#039; BI-Tools (Power BI, Grafana, ...) lesen aufbereitete Kennzahlen direkt aus OBS.&lt;br /&gt;
&lt;br /&gt;
===Was bedeutet das wirtschaftlich?===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Manuelle Arbeit fällt weg.&#039;&#039;&#039; Daten werden nicht mehr aus OBS exportiert, per Mail geschickt und woanders eingelesen - sie fliessen direkt.&lt;br /&gt;
* &#039;&#039;&#039;Fehler werden weniger.&#039;&#039;&#039; Jede manuelle Stelle ist eine mögliche Fehlerquelle; jede Automatisierung eine Stelle, an der nichts mehr schiefgehen kann.&lt;br /&gt;
* &#039;&#039;&#039;Neue Geschäftsmodelle werden möglich.&#039;&#039;&#039; Kunden- und Lieferantenportale, Self-Service-Funktionen, App-Anbindungen lassen sich aufbauen, ohne ein zweites System pflegen zu müssen.&lt;br /&gt;
* &#039;&#039;&#039;Bestehende Software bleibt verbunden.&#039;&#039;&#039; Statt eine vorhandene Anwendung ablösen zu müssen, kann sie über den REST-Server an OBS angedockt werden - in beiden Richtungen.&lt;br /&gt;
* &#039;&#039;&#039;Skaliert mit:&#039;&#039;&#039; Was klein anfängt (ein einzelner Endpunkt für eine Webseite) kann zu einer kompletten Integrations-Plattform ausgebaut werden, ohne dass die Grundlage gewechselt werden muss.&lt;br /&gt;
&lt;br /&gt;
==Architektur im Überblick==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server besteht aus mehreren aufeinander aufbauenden Bausteinen, die alle in OBS gepflegt werden:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Baustein     !! Tabelle              !! Zweck&lt;br /&gt;
|-&lt;br /&gt;
| Server       || RESTSRV_SERVER       || TLS-Profil + eigene HTTP-Server-Instanz, kann mehrfach existieren&lt;br /&gt;
|-&lt;br /&gt;
| Bindung      || RESTSRV_BINDINGS     || IP-Adresse + Port pro Server&lt;br /&gt;
|-&lt;br /&gt;
| Zugang       || RESTSRV_ACCOUNT      || API-Key, optionale Host- und CORS-Beschränkung, optionale JWT-Konfiguration&lt;br /&gt;
|-&lt;br /&gt;
| Endpunkt     || RESTSRV_ENDPOINTS    || Skript, das die Anfrage bearbeitet, einem Server fest zugeordnet&lt;br /&gt;
|-&lt;br /&gt;
| Berechtigung || RESTSRV_ACCESS       || Verbindet Zugang und Endpunkt&lt;br /&gt;
|-&lt;br /&gt;
| Idempotenz   || RESTSRV_IDEMPOTENCY  || Speicher für Idempotency-Keys und die dazu gelieferte Antwort&lt;br /&gt;
|-&lt;br /&gt;
| Sitzung      || RESTSRV_TOKEN        || Ausgestellte Token-Paare je Sitzung; Grundlage für Rotation und Widerruf&lt;br /&gt;
|-&lt;br /&gt;
| Protokoll    || RESTSRV_PROTO        || Laufende Ereignisse, Fehler, Audit-Einträge&lt;br /&gt;
|-&lt;br /&gt;
| Statistik    || RESTSRV_STATS        || Aufrufzähler, Antwortzeiten und HTTP-Status pro Endpunkt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Eine Anfrage durchläuft folgende Stationen:&lt;br /&gt;
&lt;br /&gt;
# Rate-Limit-Prüfung (pro API-Key bzw. IP und global, Schwellen je Server-Profil)&lt;br /&gt;
# Andrangsgrenze des Server-Profils (&#039;&#039;Max. parallel&#039;&#039;) - bei Überschreitung &#039;&#039;&#039;503&#039;&#039;&#039; mit &#039;&#039;Retry-After&#039;&#039;&lt;br /&gt;
# Authentifizierung über den API-Key (Header &#039;&#039;apikey&#039;&#039;)&lt;br /&gt;
# CORS-Prüfung (sofern &#039;&#039;Origin&#039;&#039;-Header gesetzt)&lt;br /&gt;
# Optional: JWT-Prüfung - Signatur, Ablauf und Sitzung (sofern für den Zugang aktiviert)&lt;br /&gt;
# Routing: Auflösung des Pfads über die Pfad-Templates der Endpunkte&lt;br /&gt;
# Prüfung der Berechtigung (Zugang -&amp;gt; Endpunkt)&lt;br /&gt;
# Entpacken des Anfragekörpers, sofern er mit &#039;&#039;Content-Encoding: gzip&#039;&#039; gesendet wurde&lt;br /&gt;
# Idempotenz-Prüfung bei POST/PUT/PATCH/DELETE - der Header &#039;&#039;Idempotency-Key&#039;&#039; ist dort &#039;&#039;&#039;Pflicht&#039;&#039;&#039;&lt;br /&gt;
# Ausführung des Endpunkt-Skripts (oder WebHook-Antwort)&lt;br /&gt;
# Festschreiben des Ergebnisses im Idempotenz-Speicher (sofern in Schritt 9 reserviert)&lt;br /&gt;
# JSON-Antwort und Statistik-Eintrag&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Die Reihenfolge ist kein Zufall.&#039;&#039;&#039; Das Entpacken liegt &#039;&#039;&#039;hinter&#039;&#039;&#039;&lt;br /&gt;
der Authentifizierung, damit ein Unbefugter dem Server keine Rechenzeit für eine&lt;br /&gt;
Dekompressionsbombe abverlangen kann - und &#039;&#039;&#039;vor&#039;&#039;&#039; der Idempotenz-Reservierung,&lt;br /&gt;
damit eine abgewiesene Anfrage keinen &#039;&#039;Idempotency-Key&#039;&#039; verbrennt.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Der Normalfall erzeugt keine Protokollzeile.&#039;&#039;&#039; Bis 08/2026 schrieb jede&lt;br /&gt;
Anfrage eine Zeile nach RESTSRV_PROTO; sie ist entfallen, weil RESTSRV_STATS&lt;br /&gt;
zu derselben Anfrage IP, Zugang, Endpunkt, Methode, Pfad, Status, Laufzeit und&lt;br /&gt;
dieselbe Korrelations-ID trägt - und länger aufbewahrt wird. Das Protokoll trägt&lt;br /&gt;
seitdem nur noch Auffälligkeiten.}}&lt;br /&gt;
&lt;br /&gt;
==Adressierung==&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt wird über ein &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; angesprochen, das den Pfad nach dem Host beschreibt und Platzhalter enthalten kann:&lt;br /&gt;
&lt;br /&gt;
 http://[Hostadresse][:Port][Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Beispiele:&lt;br /&gt;
&lt;br /&gt;
 https://api.meinserver.de/kalender/v1&lt;br /&gt;
 https://api.meinserver.de/orders/{uid}&lt;br /&gt;
 https://api.meinserver.de/orders/{uid}/modules/{code}&lt;br /&gt;
&lt;br /&gt;
Statische Segmente müssen exakt passen; Platzhalter in geschweiften Klammern (z.B. {uid}) matchen einen beliebigen Wert und stehen im Skript als Pfad-Parameter zur Verfügung. Eine Versionskennung wird als statisches Segment ins Template aufgenommen (z.B. /kalender/v1), um eine Funktionalität zu verändern, ohne bestehende Konsumenten zu brechen. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].&lt;br /&gt;
&lt;br /&gt;
==Unterstützte HTTP-Methoden==&lt;br /&gt;
&lt;br /&gt;
Der Server akzeptiert die Methoden &#039;&#039;&#039;GET&#039;&#039;&#039;, &#039;&#039;&#039;POST&#039;&#039;&#039;, &#039;&#039;&#039;PUT&#039;&#039;&#039;, &#039;&#039;&#039;DELETE&#039;&#039;&#039; und &#039;&#039;&#039;PATCH&#039;&#039;&#039; sowie &#039;&#039;&#039;OPTIONS&#039;&#039;&#039; (CORS-Preflight, ohne Skript-Aufruf). Jede Methode wird im Endpunkt-Skript als gleichnamige Prozedur implementiert - &#039;&#039;&#039;mit einer Ausnahme&#039;&#039;&#039;: &#039;&#039;Delete&#039;&#039; ist in der Skriptsprache ein reservierter Name und lässt sich nicht als Prozedur deklarieren. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
Fehlt die zur Methode passende Prozedur, antwortet der Server mit &#039;&#039;&#039;405&#039;&#039;&#039; und&lt;br /&gt;
nennt im Header &#039;&#039;Allow&#039;&#039; die Verben, die dieses Skript tatsächlich anbietet.&lt;br /&gt;
&lt;br /&gt;
==Kompression==&lt;br /&gt;
&lt;br /&gt;
Der Server packt Antworten mit &#039;&#039;&#039;gzip&#039;&#039;&#039;, wenn der Client sie annimmt. Ausgehandelt wird über den Anfrage-Header &amp;lt;code&amp;gt;Accept-Encoding&amp;lt;/code&amp;gt;; die Antwort trägt dann &amp;lt;code&amp;gt;Content-Encoding: gzip&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;Vary: Accept-Encoding&amp;lt;/code&amp;gt; - bei einer CORS-Anfrage steht dort &amp;lt;code&amp;gt;Vary: Origin, Accept-Encoding&amp;lt;/code&amp;gt;, also &#039;&#039;&#039;eine&#039;&#039;&#039; Zeile mit beiden Bedingungen. Browser, &amp;lt;code&amp;gt;curl --compressed&amp;lt;/code&amp;gt; und die gängigen HTTP-Bibliotheken entpacken selbstständig - für den Konsumenten ändert sich nichts ausser der übertragenen Datenmenge.&lt;br /&gt;
&lt;br /&gt;
Gepackt wird nur, wo es etwas bringt:&lt;br /&gt;
&lt;br /&gt;
* Nur JSON (auch &#039;&#039;problem+json&#039;&#039; und &#039;&#039;ndjson&#039;&#039;), XML, JavaScript, SVG und alles unter &#039;&#039;text/&#039;&#039; - darunter Text, CSV und HTML. Bereits komprimierte Typen - PDF, Bild, ZIP, Video - werden durch gzip &#039;&#039;&#039;grösser&#039;&#039;&#039;, nicht kleiner.&lt;br /&gt;
* Erst ab &#039;&#039;&#039;1024 Byte&#039;&#039;&#039;. Darunter kostet der gzip-Rahmen mehr, als der Inhalt einspart.&lt;br /&gt;
* &#039;&#039;&#039;Nicht&#039;&#039;&#039; bei Dateiantworten, nicht bei Teilauslieferungen (206) und nicht bei Antworten ohne Körper (204, 304).&lt;br /&gt;
* Nicht, wenn das Endpunkt-Skript den Header &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; selbst gesetzt hat - dann gehört die Antwort dem Skript.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;deflate&#039;&#039;&#039; wird in &#039;&#039;&#039;keiner&#039;&#039;&#039; Richtung unterstützt. Der Wert ist historisch uneindeutig - die einen senden RFC-1950-gerahmt, die anderen roh -, und wer ihn nennt, beherrscht ohnehin gzip. Eine Anfrage mit &amp;lt;code&amp;gt;Content-Encoding: deflate&amp;lt;/code&amp;gt; wird mit 415 abgewiesen.&lt;br /&gt;
&lt;br /&gt;
Abschalten lässt sich das Packen je Server-Profil über den Haken &#039;&#039;Keine Kompression&#039;&#039; (siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]). Der Haken betrifft ausschliesslich die Antwortrichtung.&lt;br /&gt;
&lt;br /&gt;
===Komprimierte Anfragen===&lt;br /&gt;
&lt;br /&gt;
In der Gegenrichtung nimmt der Server &#039;&#039;&#039;gzip&#039;&#039;&#039; entgegen: Trägt eine Anfrage den Header &amp;lt;code&amp;gt;Content-Encoding: gzip&amp;lt;/code&amp;gt;, wird der Körper entpackt, bevor ihn jemand liest. Das Endpunkt-Skript merkt davon nichts.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Anfrage                                              !! Antwort&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; fehlt oder &amp;lt;code&amp;gt;identity&amp;lt;/code&amp;gt; || normale Verarbeitung; ab 10 MB Körpergrösse &#039;&#039;&#039;413&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;gzip&amp;lt;/code&amp;gt;                                    || Körper wird entpackt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;deflate&amp;lt;/code&amp;gt;, anderer Wert (z.B. &amp;lt;code&amp;gt;br&amp;lt;/code&amp;gt;) oder mehrere Werte || &#039;&#039;&#039;415&#039;&#039;&#039; &amp;lt;code&amp;gt;UNSUPPORTED_MEDIA_TYPE&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Gepackter Körper bei einem Datei-Upload              || &#039;&#039;&#039;415&#039;&#039;&#039; - siehe [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]]&lt;br /&gt;
|-&lt;br /&gt;
| Entpackter Körper über &#039;&#039;&#039;10 MB&#039;&#039;&#039;                    || &#039;&#039;&#039;413&#039;&#039;&#039; &amp;lt;code&amp;gt;PAYLOAD_TOO_LARGE&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Entpackter Körper über dem &#039;&#039;&#039;400fachen&#039;&#039;&#039; der gesendeten Grösse || &#039;&#039;&#039;413&#039;&#039;&#039; &amp;lt;code&amp;gt;PAYLOAD_TOO_LARGE&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Für einen gepackten Anfragekörper gelten &#039;&#039;&#039;zwei&#039;&#039;&#039; Grenzen. Beide zählen den &#039;&#039;&#039;entpackten&#039;&#039;&#039; Inhalt, und beide greifen bereits &#039;&#039;&#039;während&#039;&#039;&#039; des Entpackens - ein Körper, der sie reisst, belegt den Speicher gar nicht erst:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;10 MB&#039;&#039;&#039; absolut. Dieselbe Grenze, die auch für einen ungepackten Körper gilt.&lt;br /&gt;
* &#039;&#039;&#039;das 400fache&#039;&#039;&#039; der gesendeten Grösse, &#039;&#039;&#039;mindestens aber 64 KB&#039;&#039;&#039;. Damit wird eine &#039;&#039;Dekompressionsbombe&#039;&#039; - wenige Kilobyte, die sich auf Hunderte Megabyte aufblähen - schon nach wenigen Prozent abgebrochen. Der Boden von 64 KB sorgt dafür, dass die Verhältnisrechnung winzige Körper gar nicht erst trifft: ein 100-Byte-Körper darf sich auf 64 KB entpacken, nicht nur auf 40 KB.&lt;br /&gt;
&lt;br /&gt;
Die zweite Grenze trifft normale Nutzlasten nicht. Gemessen erreichen echte JSON-Körper Verhältnisse von 24:1 bis 40:1, und selbst eine Liste aus zehntausenden identischen Zeilen kommt nur auf rund 340:1 - mehr gibt das Verfahren für wiederholte Datensätze nicht her. Über 400:1 kommt nur, wer sehr lange Läufe desselben Zeichens sendet.&lt;br /&gt;
&lt;br /&gt;
Für Datei-Uploads gilt keine der beiden Grenzen - dort zählt allein die je Endpunkt eingestellte Maximalgrösse.}}&lt;br /&gt;
&lt;br /&gt;
==Persistente Verbindungen (Keep-alive)==&lt;br /&gt;
&lt;br /&gt;
Der Server hält eine Verbindung nach der Antwort &#039;&#039;&#039;offen&#039;&#039;&#039;, statt sie zu&lt;br /&gt;
schliessen. Ein Client, der mehrere Anfragen hintereinander stellt - eine App,&lt;br /&gt;
die eine Liste seitenweise abholt -, spart damit je Folgeanfrage den Aufbau von&lt;br /&gt;
TCP-Verbindung und TLS-Sitzung.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Grenze !! Wert !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| Leerlauf || &#039;&#039;&#039;10 Sekunden&#039;&#039;&#039; || Passiert auf der Verbindung so lange nichts, wird sie geschlossen&lt;br /&gt;
|-&lt;br /&gt;
| Anfragen je Verbindung || &#039;&#039;&#039;100&#039;&#039;&#039; || Danach schliesst der Server geordnet, der Client baut neu auf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beides ist für den Konsumenten unkritisch: Eine geschlossene Verbindung ist kein&lt;br /&gt;
Fehler, jede HTTP-Bibliothek baut sie bei der nächsten Anfrage selbst wieder auf.&lt;br /&gt;
Wer viele Anfragen hintereinander stellt, gewinnt aber deutlich, wenn er seinen&lt;br /&gt;
Client die Verbindung wiederverwenden lässt (&#039;&#039;Connection: keep-alive&#039;&#039; ist der&lt;br /&gt;
Standard von HTTP/1.1, die meisten Bibliotheken tun es von allein).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Die Leerlaufgrenze gilt für jeden Lesevorgang der Verbindung&#039;&#039;&#039;,&lt;br /&gt;
nicht nur für das Warten auf die nächste Anfrage. Ein Client, der seinen&lt;br /&gt;
Anfragekörper nur tropfenweise sendet und dazwischen länger als 10 Sekunden&lt;br /&gt;
pausiert, verliert die Verbindung mitten im Senden.}}&lt;br /&gt;
&lt;br /&gt;
Die Werte sind bewusst knapp gewählt: Der Server fährt einen eigenen Thread je&lt;br /&gt;
Verbindung, eine leerlaufende Verbindung kostet also nicht bloss einen&lt;br /&gt;
Dateizeiger. Sie sind keine Lastgrenze - dafür gibt es &#039;&#039;Max. Verbindungen&#039;&#039; und&lt;br /&gt;
&#039;&#039;Max. parallel&#039;&#039; am Server-Profil (siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]).&lt;br /&gt;
&lt;br /&gt;
==Datei-Uploads==&lt;br /&gt;
&lt;br /&gt;
Endpunkte, die dafür freigeschaltet sind (&amp;lt;code&amp;gt;re_upload&amp;lt;/code&amp;gt;), nehmen Dateien&lt;br /&gt;
per &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; oder als fortsetzbaren &#039;&#039;&#039;Resumable-Upload&#039;&#039;&#039;&lt;br /&gt;
(&amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;) entgegen. Die maximale Grösse wird pro Endpunkt&lt;br /&gt;
festgelegt (&amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;, Standard 25&amp;amp;nbsp;MB); Überschreitung&lt;br /&gt;
ergibt 413, ein Upload an einen nicht freigeschalteten Endpunkt 415. Der Server&lt;br /&gt;
legt die Datei temporär ab und übergibt dem Skript den Pfad; die weitere&lt;br /&gt;
Verarbeitung (z.B. DMS-Ablage) übernimmt das Skript. Details:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
==Doppelte Sendungen abfangen (Idempotenz)==&lt;br /&gt;
&lt;br /&gt;
Ein Client, der nach einem Timeout dieselbe Anfrage erneut schickt, darf keine&lt;br /&gt;
Zweitbuchung auslösen. Der Server erledigt das selbst: Jeder Aufruf mit&lt;br /&gt;
&#039;&#039;&#039;POST&#039;&#039;&#039;, &#039;&#039;&#039;PUT&#039;&#039;&#039;, &#039;&#039;&#039;PATCH&#039;&#039;&#039; oder &#039;&#039;&#039;DELETE&#039;&#039;&#039; muss den Header&lt;br /&gt;
&amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; tragen; der Server merkt sich Schlüssel und Ergebnis&lt;br /&gt;
in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;. Eine Wiederholung mit demselben Schlüssel und&lt;br /&gt;
demselben Inhalt bekommt die &#039;&#039;&#039;gespeicherte Antwort&#039;&#039;&#039; zurück - das&lt;br /&gt;
Endpunkt-Skript läuft gar nicht erst an.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Der Header ist Pflicht, nicht optional.&#039;&#039;&#039; Ein schreibender Aufruf&lt;br /&gt;
&#039;&#039;&#039;ohne&#039;&#039;&#039; &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; wird mit &#039;&#039;&#039;400&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt; abgewiesen, und das Endpunkt-Skript läuft&lt;br /&gt;
nicht an.&lt;br /&gt;
&lt;br /&gt;
Das war bis v1.21 anders: fehlte der Header, lief die Anfrage ungeschützt durch.&lt;br /&gt;
Der Schutz gegen eine Doppelbuchung stand damit in der Zusage des Clients und&lt;br /&gt;
nicht in einer Prüfung des Servers - ein Wiederholversuch nach Zeitüberschreitung&lt;br /&gt;
buchte ein zweites Mal. Konsumenten, die bisher ohne den Header schreiben, müssen&lt;br /&gt;
angepasst werden.}}&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Pflicht nur für schreibende Verben.&#039;&#039;&#039; &#039;&#039;GET&#039;&#039; braucht keinen Schlüssel und kennt die Mechanik nicht.&lt;br /&gt;
* &#039;&#039;&#039;Der JWT-Endpunkt ist ausgenommen.&#039;&#039;&#039; Anmelden, Erneuern und Abmelden laufen vor der Adressauflösung und brauchen keinen Schlüssel.&lt;br /&gt;
* &#039;&#039;&#039;Kein Schalter am Endpunkt.&#039;&#039;&#039; Die Mechanik greift für jeden Endpunkt automatisch.&lt;br /&gt;
* &#039;&#039;&#039;Das Skript muss nichts tun.&#039;&#039;&#039; Eine eigene Idempotenz-Logik im Skript ist nicht nötig.&lt;br /&gt;
* &#039;&#039;&#039;Pro Vorgang ein Schlüssel&#039;&#039;&#039;, über alle Wiederholversuche hinweg unverändert - nicht bei jedem Versuch neu erzeugt.&lt;br /&gt;
&lt;br /&gt;
Details und die Statuscodes: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]], Abschnitt Idempotenz.&lt;br /&gt;
&lt;br /&gt;
==Sitzungen und Abmelden==&lt;br /&gt;
&lt;br /&gt;
Ein JWT trägt seine Gültigkeit in sich: der Server prüft Signatur und Ablauf und&lt;br /&gt;
braucht dafür keine Datenbank. Genau deshalb liesse sich ein ausgegebener Token&lt;br /&gt;
von sich aus auch nicht mehr zurückziehen - ein verlorenes Gerät wäre bis zum&lt;br /&gt;
Ablauf des Tokens weiter nutzbar.&lt;br /&gt;
&lt;br /&gt;
Der Server führt deshalb zu jedem ausgestellten Token-Paar eine Zeile in&lt;br /&gt;
&#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; und prüft jeden Zugriff dagegen. Daraus ergeben sich drei&lt;br /&gt;
Eigenschaften:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Abmelden wirkt sofort.&#039;&#039;&#039; Ein &#039;&#039;DELETE&#039;&#039; auf den JWT-Endpunkt sperrt die Sitzung; alle Token dieser Sitzung sind damit unmittelbar ungültig, auch die noch nicht abgelaufenen. Anmelden, Erneuern und Abmelden laufen also über einen einzigen Endpunkt, und für das Abmelden ist kein Skript nötig.&lt;br /&gt;
* &#039;&#039;&#039;Ein Refresh-Token gilt genau einmal.&#039;&#039;&#039; Beim Erneuern wird es entwertet. Wird es später erneut vorgelegt, gilt das als Diebstahl oder Fehlfunktion: die betroffene Sitzung wird komplett gesperrt und der Vorfall protokolliert.&lt;br /&gt;
* &#039;&#039;&#039;Alle Geräte abmelden.&#039;&#039;&#039; Ein gewöhnlicher Endpunkt kann über den Aufruf &#039;&#039;oWriter.JwtRevoke(&#039;all&#039;)&#039;&#039; auch alle Sitzungen eines Benutzers sperren - der Support-Fall „Gerät verloren&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Mit der Einführung dieser Prüfung verlieren &#039;&#039;&#039;alle vorher&lt;br /&gt;
ausgestellten Token&#039;&#039;&#039; ihre Gültigkeit, weil sie zu keiner Sitzung gehören.&lt;br /&gt;
Angemeldete Konsumenten müssen sich nach dem Update einmalig neu anmelden.}}&lt;br /&gt;
&lt;br /&gt;
Der Speicher pflegt sich selbst: Zeilen werden gelöscht, sobald sowohl das&lt;br /&gt;
Access- als auch das Refresh-Token abgelaufen sind. Details und die Skript-Seite:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Fehlerformat==&lt;br /&gt;
&lt;br /&gt;
Jede Fehlerantwort des Servers hat dieselbe Form: ein einziges &#039;&#039;error&#039;&#039;-Objekt&lt;br /&gt;
mit maschinenlesbarem Code, Support-Kennung, Meldung und Korrelations-ID:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;error&amp;quot;: {&lt;br /&gt;
    &amp;quot;code&amp;quot;:    &amp;quot;AUTH_EXPIRED&amp;quot;,&lt;br /&gt;
    &amp;quot;uid&amp;quot;:     &amp;quot;Y00E2RTPDY&amp;quot;,&lt;br /&gt;
    &amp;quot;message&amp;quot;: &amp;quot;Ungültiges oder abgelaufenes Token&amp;quot;,&lt;br /&gt;
    &amp;quot;traceId&amp;quot;: &amp;quot;20260817T091233123-00001A&amp;quot;&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;code&#039;&#039;&#039; - sprechender Bezeichner (z.B. &#039;&#039;AUTH_EXPIRED&#039;&#039;, &#039;&#039;NOT_FOUND&#039;&#039;, &#039;&#039;RATE_LIMITED&#039;&#039;, &#039;&#039;INTERNAL_ERROR&#039;&#039;), an dem ein Client seine Reaktion festmachen kann, ohne Texte auszuwerten.&lt;br /&gt;
* &#039;&#039;&#039;uid&#039;&#039;&#039; - OBS-Kennung der Codestelle. Für die Support-Recherche gedacht, nicht zur Auswertung im Client.&lt;br /&gt;
* &#039;&#039;&#039;message&#039;&#039;&#039; - Text für den Client. Interne Fehlerdetails stehen nie darin, sondern ausschliesslich im Protokoll &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;traceId&#039;&#039;&#039; - Korrelations-ID. Steht zusätzlich im Header &#039;&#039;X-Trace-Id&#039;&#039; und in jeder zugehörigen Logzeile.&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt-Skript kann eigene Codes setzen; siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;Fehlercode&#039;&#039;, &#039;&#039;Nachricht&#039;&#039; und &#039;&#039;traceId&#039;&#039; stehen &#039;&#039;&#039;nur&#039;&#039;&#039; im&lt;br /&gt;
&#039;&#039;error&#039;&#039;-Objekt, nicht flach daneben - der Wert von&lt;br /&gt;
&#039;&#039;Fehlercode&#039;&#039; heisst jetzt &#039;&#039;error.uid&#039;&#039;, &#039;&#039;Nachricht&#039;&#039; entspricht&lt;br /&gt;
&#039;&#039;error.message&#039;&#039;. Clients, die die flachen Felder auswerten, müssen angepasst&lt;br /&gt;
werden.}}&lt;br /&gt;
&lt;br /&gt;
==Sicherheitsmerkmale==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server enthält eine Reihe von Schutzmechanismen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Rate-Limit:&#039;&#039;&#039; Standardmässig max. 10 fehlgeschlagene und 120 erfolgreiche Anfragen pro API-Key (Account) bzw. IP und 60 Sekunden, max. 600 Anfragen global pro 60 Sekunden. Beim Überschreiten wird HTTP 429 mit &#039;&#039;Retry-After&#039;&#039;-Header zurückgegeben. Die vier Schwellen lassen sich &#039;&#039;&#039;pro Server-Profil&#039;&#039;&#039; hinterlegen; die Zähler laufen ebenfalls getrennt je Profil, ein überlastetes Profil bremst das andere also nicht mit aus. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
* &#039;&#039;&#039;Body-Limit:&#039;&#039;&#039; max. 10 MB Request-Body (JSON), max. 1024 Zeichen pro Header- oder Query-Parameter. Datei-Uploads laufen über einen separaten Pfad und sind pro Endpunkt auf &amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt; MB begrenzt (Standard 25&amp;amp;nbsp;MB).&lt;br /&gt;
* &#039;&#039;&#039;Sensible Felder&#039;&#039;&#039; (&#039;&#039;password&#039;&#039;, &#039;&#039;token&#039;&#039;, &#039;&#039;secret&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, ...) werden in Protokoll-Ausgaben automatisch maskiert.&lt;br /&gt;
* &#039;&#039;&#039;Widerrufbare Token:&#039;&#039;&#039; Jedes ausgestellte Token-Paar gehört zu einer Sitzung in &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; und wird bei &#039;&#039;&#039;jedem&#039;&#039;&#039; Zugriff dagegen geprüft. Eine Abmeldung wirkt damit sofort und nicht erst mit dem Ablauf des Tokens; ein Refresh-Token gilt genau einmal. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
* &#039;&#039;&#039;Reserviertes Präfix:&#039;&#039;&#039; Parameter-Namen mit Präfix &#039;&#039;_OBS_&#039;&#039; können von aussen nicht gesetzt werden, sie sind für den Server reserviert (z.B. JWT-Claims).&lt;br /&gt;
* &#039;&#039;&#039;Geblockte Header:&#039;&#039;&#039; &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, &#039;&#039;proxy-authorization&#039;&#039;, &#039;&#039;x-forwarded-for&#039;&#039;, &#039;&#039;x-real-ip&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;api_key&#039;&#039; werden nicht an das Skript durchgereicht.&lt;br /&gt;
* &#039;&#039;&#039;TLS:&#039;&#039;&#039; Bei Profilen mit aktivem TLS fährt der Server ohne Zertifikat und Key nicht hoch. Plain-HTTP ist nur für ein dediziertes Debug-Profil möglich.&lt;br /&gt;
* &#039;&#039;&#039;HSTS:&#039;&#039;&#039; Antworten eines TLS-Profils tragen &#039;&#039;Strict-Transport-Security: max-age=31536000; includeSubDomains&#039;&#039;. Ein Browser spricht die Adresse danach ein Jahr lang nur noch über HTTPS an. Beim Debug-Profil &#039;&#039;Kein SSL&#039;&#039; wird der Header bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; gesetzt - der Browser würde ihn sonst speichern und die Erzwingung auch auf produktive Sitzungen anwenden.&lt;br /&gt;
* &#039;&#039;&#039;mTLS&#039;&#039;&#039; (Mutual TLS): erzwingt, dass jeder Client ein gültiges Client-Zertifikat vorlegt. Optional kann pro Zugang ein erwarteter Subject-DN hinterlegt werden.&lt;br /&gt;
* &#039;&#039;&#039;DNS-Cache:&#039;&#039;&#039; Host-zu-IP-Auflösungen für die Zugangs-Host-Prüfung werden 5 Minuten gecached.&lt;br /&gt;
* &#039;&#039;&#039;Automatische Zertifikatserneuerung:&#039;&#039;&#039; Ein TLS-Profil kann sein Zertifikat selbst bei einer Zertifizierungsstelle beantragen und vor Ablauf erneuern. Der Dienst prüft wöchentlich, ob der Server dafür von aussen erreichbar ist, und tauscht das Zertifikat anschliessend ohne Neustart des Dienstes - nur das betroffene Profil startet neu. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
&lt;br /&gt;
==Protokoll==&lt;br /&gt;
&lt;br /&gt;
Die Tabelle &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; enthält alle relevanten Ereignisse: erfolgreiche und abgelehnte Anfragen, TLS-/JWT-Fehler, Bindungs-Ereignisse, Skript-Fehler. Einträge sind nach Datum, Uhrzeit und Host (IP oder Account-Name) sortiert. Das Protokoll ist die erste Anlaufstelle bei Störungen. Jede Antwort trägt zudem eine Korrelations-ID (Response-Header &#039;&#039;X-Trace-Id&#039;&#039;), die in den Protokoll-Einträgen wiederzufinden ist.&lt;br /&gt;
&lt;br /&gt;
Läuft der REST-Server hinter einem Reverse-Proxy, steht als Host die IP-Adresse des Proxys. Schickt der Proxy den Header &#039;&#039;X-Forwarded-For&#039;&#039; mit, hängt der Server dessen Wert in Protokoll und Konsole als &amp;lt;code&amp;gt;[xff=…]&amp;lt;/code&amp;gt; an den Eintrag an. Der Wert ist eine Angabe des Absenders und wird nur protokolliert. Host-Beschränkung und Rate-Limit verwenden immer die IP-Adresse der Verbindung. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung#Betrieb hinter einem Reverse-Proxy|Betrieb hinter einem Reverse-Proxy]].&lt;br /&gt;
&lt;br /&gt;
==Statistik==&lt;br /&gt;
&lt;br /&gt;
Die Tabelle &#039;&#039;&#039;RESTSRV_STATS&#039;&#039;&#039; enthält eine Statistik aller bearbeiteten Anfragen&lt;br /&gt;
mit IP, Zugang, Endpunkt, Methode, angefragtem Pfad, HTTP-Status und Laufzeit in&lt;br /&gt;
Millisekunden. Dazu kommen zwei Felder, die den Sprung ins Protokoll erlauben:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;rs_trace&#039;&#039;&#039; - dieselbe Korrelations-ID, die auch im Header &#039;&#039;X-Trace-Id&#039;&#039; und in jeder zugehörigen Protokollzeile steht. Damit findet man zu einer auffälligen Statistikzeile alle Protokolleinträge derselben Anfrage.&lt;br /&gt;
* &#039;&#039;&#039;rs_server&#039;&#039;&#039; - das Server-Profil, über das die Anfrage hereinkam. Bei mehreren Profilen (Public-TLS neben Internal-mTLS) ist das die einzige Angabe, an der sich der Weg ablesen lässt.&lt;br /&gt;
&lt;br /&gt;
Weil der Normalfall keine Protokollzeile mehr schreibt, ist RESTSRV_STATS die&lt;br /&gt;
vollständige Sicht auf den Betrieb - RESTSRV_PROTO die auf die Auffälligkeiten.&lt;br /&gt;
&lt;br /&gt;
Aufruf der Statistik:&lt;br /&gt;
&lt;br /&gt;
* Aus der Endpunkt-Liste: F8 zeigt die Statistik des markierten Endpunkts.&lt;br /&gt;
* Aus der Zugänge-Liste: F8 zeigt die Statistik des markierten Zugangs.&lt;br /&gt;
&lt;br /&gt;
==Konsole==&lt;br /&gt;
&lt;br /&gt;
Wird der REST-Server nicht als Dienst, sondern interaktiv gestartet, erscheint eine&lt;br /&gt;
farbige Konsole mit Live-Log und einem &#039;&#039;&#039;fixierten Kopfbereich&#039;&#039;&#039;, der die gerade&lt;br /&gt;
laufenden Anfragen zeigt und sich fortlaufend aktualisiert.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;show &amp;lt;handle&amp;gt;&#039;&#039; || Details einer Anfrage: Request-Header, Protokollzeilen, Antwort. JSON wird farbig formatiert. &#039;&#039;&amp;lt;handle&amp;gt;&#039;&#039; ist die Kennung aus der Abschlusszeile der Anfrage (z.B. &#039;&#039;7F&#039;&#039;), &#039;&#039;&#039;keine&#039;&#039;&#039; laufende Nummer&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;1&#039;&#039; … &#039;&#039;9&#039;&#039; || Auswahl im Kopfbereich - kürzer als &#039;&#039;show&#039;&#039; für die gerade sichtbaren Anfragen. Mit &#039;&#039;mouse on&#039;&#039; auch per Klick&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;ps&#039;&#039; || Listet die gerade laufenden Anfragen auf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;level &amp;lt;stufe&amp;gt;&#039;&#039; || Anzeigestufe des Live-Logs: &#039;&#039;quiet&#039;&#039; , &#039;&#039;normal&#039;&#039; oder &#039;&#039;debug&#039;&#039;. Ohne Parameter wird die aktuelle Stufe genannt&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;filter &amp;lt;ausdruck&amp;gt;&#039;&#039; || Blendet aus, was nicht passt: &#039;&#039;path=&amp;lt;text&amp;gt;&#039;&#039;, &#039;&#039;account=&amp;lt;name&amp;gt;&#039;&#039;, &#039;&#039;status&amp;gt;=&amp;lt;code&amp;gt;&#039;&#039;. &#039;&#039;filter aus&#039;&#039; hebt die Einschränkung auf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;panel on&amp;amp;#124;off&#039;&#039; || Fixierten Kopfbereich ein-/ausschalten. &#039;&#039;&#039;Ausgeschaltet&#039;&#039;&#039; lässt sich der Rückblick-Puffer des Konsolenfensters wieder scrollen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;mouse on&amp;amp;#124;off&#039;&#039; || Klick-Auswahl im Kopfbereich. &#039;&#039;&#039;Eingeschaltet&#039;&#039;&#039; ist das Markieren von Text mit der Maus nicht mehr möglich&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;help&#039;&#039; / &#039;&#039;?&#039;&#039; || Zeigt diese Liste in der Konsole&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;exit&#039;&#039; / &#039;&#039;quit&#039;&#039; || Beendet den Server geordnet&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Anzeigefilter wirkt &#039;&#039;&#039;nur auf die Anzeige&#039;&#039;&#039;. Was die Konsole&lt;br /&gt;
ausblendet, steht vollständig in RESTSRV_PROTO - es geht nichts verloren.}}&lt;br /&gt;
&lt;br /&gt;
Im Service-Modus (Windows-Dienst) ist die Konsole nicht sichtbar. Alle Einträge landen dort ausschliesslich in der Tabelle RESTSRV_PROTO.&lt;br /&gt;
&lt;br /&gt;
==Weiterführende Seiten==&lt;br /&gt;
&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]] - Schritt-für-Schritt-Anleitung&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]] - TLS, mTLS und Bindungen&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]] - Portfreigabe, Reverse-Proxy oder VPN im Vergleich&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]] - automatische Zertifikate über http-01 oder dns-01&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]] - API-Keys, JWT, CORS, Host-Beschränkung&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]] - Verwaltung, Berechtigungen, WebHooks&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]] - Aufbau der Endpunkt-Skripte&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]] - Übertragungsprotokoll für Dateien (Multipart, resumable)&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]] - Dateien ausliefern, Range/Resume, öffentliche Artefakte&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel1|Beispiel 1: Daten-Abruf mit JWT]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel2|Beispiel 2: Zeiterfassung als komplette Webanwendung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel3|Beispiel 3: Datensatz anlegen mit JSON-Body (CRUD)]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4: Pfad-Parameter im Routing]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel5|Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6: Datei-Upload und Ablage im DMS]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=67063</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Einrichtung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=67063"/>
		<updated>2026-10-26T09:31:37Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Verweise auf Zertifikatserneuerung und Öffentliche Freigabe ergänzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Einrichtung eines REST-Endpunktes=&lt;br /&gt;
&lt;br /&gt;
Die folgende Reihenfolge stellt sicher, dass ein lauffähiger Endpunkt mit möglichst wenigen Korrektur-Schleifen entsteht.&lt;br /&gt;
&lt;br /&gt;
==Vorbereitung==&lt;br /&gt;
&lt;br /&gt;
# OBS-Support kontaktieren und das REST-Server-Modul aktivieren lassen.&lt;br /&gt;
# Prüfen, ob auf dem Zielrechner (OBS-Server oder dedizierter REST-Server) Port und IP-Adresse frei sind.&lt;br /&gt;
# Bei TLS-Profilen: TLS-Zertifikat (PEM, mit Kette), privaten Schlüssel (PEM) und - falls mTLS - das CA-Root-Zertifikat bereithalten. Alternativ nutzen Sie die [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|automatische Zertifikatserneuerung]]. Wie der Server von aussen erreichbar wird und welche Variante sich eignet, beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 1: Server-Profil anlegen==&lt;br /&gt;
&lt;br /&gt;
In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server&#039;&#039;&#039; den Punkt &#039;&#039;&#039;Server&#039;&#039;&#039; öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; ein neues Profil anlegen. Pflichtfelder:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Nr&#039;&#039;&#039; - eindeutige Nummer (1-99), wird beim Anlegen automatisch vorgeschlagen.&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - sprechender Name (z.B. &#039;&#039;Public-API&#039;&#039;, &#039;&#039;Internal-mTLS&#039;&#039;, &#039;&#039;Debug-Local&#039;&#039;).&lt;br /&gt;
* Auswahl des Modus:&lt;br /&gt;
** &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039; (Default): Zertifikat und Schlüssel hinterlegen - oder auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; die automatische Erneuerung aktivieren.&lt;br /&gt;
** &#039;&#039;&#039;mTLS-Modus aktiv&#039;&#039;&#039; setzen: zusätzlich das CA-Root-Zertifikat hinterlegen, gegen das die Client-Zertifikate geprüft werden. Jeder Client muss dann ein gültiges Zertifikat vorlegen.&lt;br /&gt;
** &#039;&#039;&#039;kein SSL&#039;&#039;&#039; setzen: ausschliesslich für lokale Tests, der Server akzeptiert dann nur Plain-HTTP.&lt;br /&gt;
&lt;br /&gt;
Optional lassen sich am Profil eigene &#039;&#039;&#039;Rate-Limit-Schwellen&#039;&#039;&#039; hinterlegen. Ohne&lt;br /&gt;
Angabe gelten die Standardwerte (60-Sekunden-Fenster, 120 erfolgreiche und 10&lt;br /&gt;
fehlgeschlagene Anfragen je Schlüssel, 600 Anfragen je Profil). Für Anwendungen&lt;br /&gt;
ohne eigenen API-Key - typisch mobile Apps - sollten die Werte bewusst gesetzt&lt;br /&gt;
werden, weil dann alle Benutzer hinter einer IP auf denselben Zähler laufen.&lt;br /&gt;
&lt;br /&gt;
Details siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 2: Bindung anlegen==&lt;br /&gt;
&lt;br /&gt;
Im Server-Profil mit &#039;&#039;&#039;F6&#039;&#039;&#039; die Bindungs-Liste öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; eine neue Bindung anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - IP-Adresse, auf der gelauscht werden soll (z.B. &#039;&#039;0.0.0.0&#039;&#039; für alle Adressen, &#039;&#039;127.0.0.1&#039;&#039; für lokal).&lt;br /&gt;
* &#039;&#039;&#039;Port&#039;&#039;&#039; - Port-Nummer (typisch 443 für HTTPS, 8099 für Debug, ggf. abweichend). {{Achtung|Port 8080 wird für RustDesk verwendet und darf daher nicht verwendet werden}}&lt;br /&gt;
* &#039;&#039;&#039;Standard&#039;&#039;&#039; setzen, wenn diese Bindung die Standard-Bindung des Servers ist (pro Server-Profil nur eine).&lt;br /&gt;
&lt;br /&gt;
==Schritt 3: REST-Dienst starten / neustarten==&lt;br /&gt;
&lt;br /&gt;
Damit Änderungen an Server-Profilen oder Bindungen wirksam werden, muss der OBS REST-Server-Dienst (oder die Konsole) gestartet bzw. neugestartet werden. Das Profil wird beim Start aus der Datenbank gelesen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 4: Zugang anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Zugänge&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Zugang anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - erscheint im Protokoll und in der Statistik.&lt;br /&gt;
* &#039;&#039;&#039;API-Key&#039;&#039;&#039; - eindeutig, mit dem Schlüsselsymbol kann ein zufälliger Key generiert werden.&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - optional die IP oder der Hostname des Konsumenten (sollte, wenn möglich, immer gesetzt werden).&lt;br /&gt;
* Optional: &#039;&#039;&#039;CORS-Origins&#039;&#039;&#039; und &#039;&#039;&#039;JWT-Konfiguration&#039;&#039;&#039;, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 5: Endpunkt anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Endpunkte&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Endpunkt anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Endpunkt&#039;&#039;&#039; - Ressourcen-/Anzeigename (z.B. &#039;&#039;orders&#039;&#039;, &#039;&#039;kalender&#039;&#039;); dient nur der Übersicht, nicht dem Routing.&lt;br /&gt;
* &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; - der maßgebliche Routing-Pfad, ggf. mit Platzhaltern (z.B. &#039;&#039;/orders&#039;&#039;, &#039;&#039;/orders/{uid}&#039;&#039;, &#039;&#039;/orders/{uid}/modules/{code}&#039;&#039;).&lt;br /&gt;
* &#039;&#039;&#039;Server&#039;&#039;&#039; - die Server-Auswahl bestimmt, über welches Server-Profil der Endpunkt erreichbar ist.&lt;br /&gt;
* &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; - nur setzen, wenn der Endpunkt Dateien entgegennehmen soll; dann auch die &#039;&#039;&#039;Max. Upload-Grösse&#039;&#039;&#039; in MB festlegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6]]).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Eine Versionskennung wird als statisches Segment ins Pfad-Template aufgenommen (z.B. &#039;&#039;/orders/v1&#039;&#039;); ein eigenes Versionsfeld gibt es nicht. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].}}&lt;br /&gt;
&lt;br /&gt;
Anschliessend mit &#039;&#039;&#039;F7&#039;&#039;&#039; das Endpunkt-Skript pflegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]]).&lt;br /&gt;
&lt;br /&gt;
==Schritt 6: Zugang für den Endpunkt freischalten==&lt;br /&gt;
&lt;br /&gt;
Aus der Endpunkt-Liste den neuen Endpunkt markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die Berechtigungs-Liste. Mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; kann ein oder mehrere Zugänge ausgewählt und mit &#039;&#039;&#039;F2&#039;&#039;&#039; übernommen werden. Erst nach dieser Freischaltung darf ein Zugang den Endpunkt aufrufen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 7: Funktionsprüfung==&lt;br /&gt;
&lt;br /&gt;
Aufruf mit einem HTTP-Client (curl, Postman, Browser-Plug-in):&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; https://api.meinserver.de/[Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Bei korrektem Setup erscheint die JSON-Antwort des Skripts. Fehlerfälle werden in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; protokolliert.&lt;br /&gt;
&lt;br /&gt;
==Schritt 8: Statistik und Protokoll prüfen==&lt;br /&gt;
&lt;br /&gt;
* In der Endpunkt-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Endpunkts.&lt;br /&gt;
* In der Zugänge-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Zugangs.&lt;br /&gt;
* In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Protokoll&#039;&#039;&#039; die Ereignisse prüfen.&lt;br /&gt;
&lt;br /&gt;
==Betrieb hinter einem Reverse-Proxy==&lt;br /&gt;
&lt;br /&gt;
Läuft der REST-Server auf dem OBS-Server, soll dieser Server meist nicht direkt aus dem Internet erreichbar sein. Vor- und Nachteile gegenüber Portfreigabe und VPN beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]]. Dann nimmt ein Reverse-Proxy die Anfragen entgegen, zum Beispiel in einer DMZ oder als Funktion der Firewall, und reicht sie an den REST-Server weiter. Von aussen ist nur der Proxy erreichbar.&lt;br /&gt;
&lt;br /&gt;
Am Proxy und im Netz richten Sie ein:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Öffentlicher Name:&#039;&#039;&#039; zeigt auf die öffentliche IP-Adresse des Proxys.&lt;br /&gt;
* &#039;&#039;&#039;Zertifikat:&#039;&#039;&#039; Der Proxy braucht ein öffentlich gültiges Zertifikat für diesen Namen. Die automatische Zertifikatserneuerung des REST-Servers wird nicht benötigt.&lt;br /&gt;
* &#039;&#039;&#039;Weiterleitung:&#039;&#039;&#039; Der Proxy leitet per HTTPS an &amp;lt;code&amp;gt;https://&amp;lt;OBS-Server&amp;gt;:&amp;lt;Port&amp;gt;&amp;lt;/code&amp;gt; weiter. Für diese Strecke genügt am REST-Server ein Zertifikat Ihrer internen Zertifizierungsstelle oder ein selbst signiertes, dem der Proxy vertraut.&lt;br /&gt;
* &#039;&#039;&#039;Header:&#039;&#039;&#039; Alle HTTP-Methoden und Header unverändert durchreichen, insbesondere &#039;&#039;Authorization&#039;&#039;, &#039;&#039;apikey&#039;&#039; und &#039;&#039;Idempotency-Key&#039;&#039;. Antworten nicht zwischenspeichern. Den Header &#039;&#039;X-Forwarded-For&#039;&#039; mit der IP-Adresse des Konsumenten setzen.&lt;br /&gt;
* &#039;&#039;&#039;Anfragegrösse:&#039;&#039;&#039; Die maximale Anfragegrösse am Proxy darf nicht unter der Upload-Grenze der Endpunkte liegen.&lt;br /&gt;
* &#039;&#039;&#039;Firewall:&#039;&#039;&#039; Den Port des REST-Servers intern nur für den Proxy freigeben.&lt;br /&gt;
&lt;br /&gt;
Hinter dem Proxy verhält sich der REST-Server so:&lt;br /&gt;
&lt;br /&gt;
* Protokoll und Konsole zeigen als Host die IP-Adresse des Proxys und hängen den Wert von &#039;&#039;X-Forwarded-For&#039;&#039; als &amp;lt;code&amp;gt;[xff=…]&amp;lt;/code&amp;gt; an.&lt;br /&gt;
* Die [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge#Host-Beschränkung|Host-Beschränkung]] eines Zugangs kann keine einzelnen Konsumenten unterscheiden.&lt;br /&gt;
* Beim [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile#Rate-Limit je Profil|Rate-Limit]] teilen sich alle Konsumenten ohne API-Key einen Zähler.&lt;br /&gt;
&lt;br /&gt;
==Wartung der Server-Tabellen==&lt;br /&gt;
&lt;br /&gt;
Die Tabellen des REST-Servers pflegen sich selbst; ein regelmässiger Eingriff ist&lt;br /&gt;
nicht vorgesehen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Tabelle !! Aufräumen&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_IDEMPOTENCY || Abgeschlossene Einträge werden nach 30 Tagen gelöscht. Offene Reservierungen werden ab 24 Stunden im Protokoll gemeldet und &#039;&#039;&#039;anschliessend ebenfalls gelöscht&#039;&#039;&#039; - sie sind trotzdem ein Support-Fall, weil die Fachwirkung unbekannt ist, siehe Stolpersteine&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_TOKEN || Zeilen werden gelöscht, sobald Access- &#039;&#039;&#039;und&#039;&#039;&#039; Refresh-Laufzeit vorbei sind. Läuft beim Serverstart und danach höchstens stündlich&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_PROTO / RESTSRV_STATS || Wachsen mit dem Betrieb. Bei hohem Aufkommen empfiehlt sich ein turnusmässiges Archivieren&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Häufige Stolpersteine==&lt;br /&gt;
&lt;br /&gt;
* Bindung angelegt, aber der Server wurde nicht neugestartet -&amp;gt; die Bindung ist noch nicht aktiv.&lt;br /&gt;
* Endpunkt einem falschen Server zugeordnet -&amp;gt; Aufruf liefert 404 &#039;&#039;Endpunkt nicht vorhanden oder inaktiv&#039;&#039;.&lt;br /&gt;
* Pfad-Template falsch geschrieben oder Segmentzahl passt nicht -&amp;gt; kein Template matcht, Aufruf liefert 404.&lt;br /&gt;
* Zugang für den Endpunkt nicht freigeschaltet -&amp;gt; Aufruf liefert 403 &#039;&#039;Keine Berechtigung für die Endpunkt-Nutzung&#039;&#039;.&lt;br /&gt;
* Falscher API-Key oder falscher Host beim Zugang -&amp;gt; Aufruf liefert 401 bzw. 403.&lt;br /&gt;
* mTLS aktiv, aber kein Client-Zertifikat installiert -&amp;gt; Verbindung wird während des TLS-Handshakes abgebrochen.&lt;br /&gt;
* Datei-Upload ohne gesetzten Haken &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; am Endpunkt -&amp;gt; Aufruf liefert 415, das Skript läuft nicht an.&lt;br /&gt;
* Skript geändert, aber der Aufruf verhält sich noch wie vorher -&amp;gt; Endpunkt-Cache abwarten (bis zu 60 Sekunden).&lt;br /&gt;
* Schreibaufruf (POST/PUT/PATCH/DELETE) liefert 400 &#039;&#039;IDEMPOTENCY_KEY_MISSING&#039;&#039; -&amp;gt; der Client sendet keinen &#039;&#039;Idempotency-Key&#039;&#039;. Der Header ist seit v1.21 &#039;&#039;&#039;Pflicht&#039;&#039;&#039;, nicht optional; das Endpunkt-Skript läuft in diesem Fall gar nicht an. Betrifft typischerweise Konsumenten, die vor dem Update gebaut wurden.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 422 &#039;&#039;IDEMPOTENCY_KEY_REUSED&#039;&#039; -&amp;gt; der Client sendet denselben &#039;&#039;Idempotency-Key&#039;&#039; mit geändertem Inhalt. Pro Vorgang genau ein Schlüssel, über alle Wiederholversuche hinweg unverändert.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 409 &#039;&#039;IDEMPOTENCY_UNRESOLVED&#039;&#039; -&amp;gt; ein früherer Versuch wurde hart abgebrochen (Dienst beendet). Ob gebucht wurde, ist offen: im Protokoll und in den Fachdaten prüfen. Der Aufräumlauf meldet den Eintrag nach 24 Stunden im Protokoll und löscht ihn dann; wer schneller weitermachen will, entfernt ihn in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;.&lt;br /&gt;
* Nach einem Update antworten &#039;&#039;&#039;alle&#039;&#039;&#039; bestehenden Token mit 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; -&amp;gt; erwartetes Verhalten. Token, die vor der Einführung der Sitzungsprüfung ausgestellt wurden, gehören zu keiner Sitzung; die Konsumenten müssen sich einmalig neu anmelden.&lt;br /&gt;
* Anmeldung liefert 503 statt eines Tokens -&amp;gt; die Sitzungszeile konnte nicht geschrieben werden. Ursache im Protokoll unter &#039;&#039;TokenStore: ...&#039;&#039; nachsehen; typisch ist eine fehlende Tabelle &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; nach einem Update ohne Datenbank-Abgleich.&lt;br /&gt;
* Abmelden liefert 503 -&amp;gt; die Sitzung liess sich nicht sperren. Der Client ist &#039;&#039;&#039;nicht&#039;&#039;&#039; abgemeldet und muss den Aufruf wiederholen; Details im Protokoll.&lt;br /&gt;
* Techniker wird beim Arbeiten unerwartet abgemeldet -&amp;gt; im Protokoll nach &#039;&#039;WIEDERVORLAGE&#039;&#039; suchen. Ein doppelt verwendetes Refresh-Token sperrt die Sitzung. Legt die App Token mehrfach parallel ab, gehört das clientseitig behoben.&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=67062</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Server-Profile</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=67062"/>
		<updated>2026-10-26T09:31:36Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Zertifikatserneuerung auf eigene Seite verlagert, Felder der Registerkarte DNS-Prüfung ergänzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Server-Profile=&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Server&#039;&#039;&#039; im REST-Modul ist ein eigenständiges TLS-Profil mit einer eigenen HTTP-Server-Instanz. In OBS können beliebig viele Server-Profile parallel betrieben werden, jedes mit eigenem Modus, eigenen Zertifikaten und eigenen Bindungen.&lt;br /&gt;
&lt;br /&gt;
Typische Einsatzfälle für mehrere Profile:&lt;br /&gt;
&lt;br /&gt;
* Ein &#039;&#039;&#039;Public-Profil&#039;&#039;&#039; mit Standard-TLS auf Port 443 für externe Konsumenten.&lt;br /&gt;
* Ein &#039;&#039;&#039;Internal-mTLS-Profil&#039;&#039;&#039; auf einem internen Port für Microservices, die zwingend ein Client-Zertifikat vorlegen müssen.&lt;br /&gt;
* Ein &#039;&#039;&#039;Debug-Profil&#039;&#039;&#039; mit Plain-HTTP auf 127.0.0.1, ausschliesslich für lokale Tests.&lt;br /&gt;
&lt;br /&gt;
==Aufruf==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Server&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Felder des Server-Profils==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Nr                  || Eindeutige Nummer 1-99, einmalig vergeben, kann später nicht mehr geändert werden&lt;br /&gt;
|-&lt;br /&gt;
| Name                || Sprechender Name, eindeutig - erscheint in Protokoll, Statistik und Endpunkt-Zuordnung&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv               || Profil wird beim Start des REST-Dienstes geladen. Inaktive Profile starten nicht&lt;br /&gt;
|-&lt;br /&gt;
| zusätzliche Infos   || Freitext zur Dokumentation (interner Kommentar, wird nicht ausgewertet)&lt;br /&gt;
|-&lt;br /&gt;
| kein SSL            || Plain-HTTP-Modus, ausschliesslich für Debug. TLS-Felder werden ignoriert&lt;br /&gt;
|-&lt;br /&gt;
| mTLS-Modus aktiv    || Mutual TLS: jeder Client muss ein gültiges Client-Zertifikat vorlegen&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikat (cert)   || PEM-Inhalt des Server-Zertifikats (inkl. ggf. Zwischen-CA-Kette)&lt;br /&gt;
|-&lt;br /&gt;
| Schlüssel (key)     || PEM-Inhalt des privaten Schlüssels&lt;br /&gt;
|-&lt;br /&gt;
| Root-Zertifikat (ca) || PEM-Inhalt des CA-Zertifikats, gegen das &#039;&lt;br /&gt;
|-&lt;br /&gt;
| Rate-Limit-Fenster  || Länge des Rate-Limit-Fensters in Sekunden. Leer/0 = Standard 60&lt;br /&gt;
|-&lt;br /&gt;
| Erfolgsanfragen     || Erlaubte erfolgreiche Anfragen je Schlüssel und Fenster. Leer/0 = Standard 120&lt;br /&gt;
|-&lt;br /&gt;
| Fehlanfragen        || Erlaubte fehlgeschlagene Anfragen je Schlüssel und Fenster. Leer/0 = Standard 10&lt;br /&gt;
|-&lt;br /&gt;
| Anfragen            || Erlaubte Anfragen des gesamten Profils je Fenster. Leer/0 = Standard 600&lt;br /&gt;
|-&lt;br /&gt;
| max. Verbindungen   || Obergrenze gleichzeitiger &#039;&#039;&#039;Verbindungen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_conn&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Sie deckt auch laufende Datei-Downloads ab, weil der Platz erst beim Trennen frei wird. Bei Überschreitung trennt der Server &#039;&#039;&#039;ohne HTTP-Antwort&#039;&#039;&#039; - der Wert gehört deutlich über den Normalbetrieb, als Reissleine&lt;br /&gt;
|-&lt;br /&gt;
| max. Anfragen       || Obergrenze gleichzeitig &#039;&#039;&#039;verarbeiteter Anfragen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_parallel&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Bei Überschreitung antwortet der Server &#039;&#039;&#039;503&#039;&#039;&#039; mit &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;, der Client darf es also gleich erneut versuchen&lt;br /&gt;
|-&lt;br /&gt;
| keine Kompression   || Antworten dieses Profils werden &#039;&#039;&#039;nicht&#039;&#039;&#039; gepackt (&amp;lt;code&amp;gt;rsv_no_gzip&amp;lt;/code&amp;gt;). Ohne Haken packt der Server JSON- und Textantworten ab 1024 Byte mit gzip, sofern der Client sie über &amp;lt;code&amp;gt;Accept-Encoding&amp;lt;/code&amp;gt; annimmt. Der Haken betrifft nur die Antwortrichtung - komprimierte &#039;&#039;&#039;Anfragen&#039;&#039;&#039; nimmt der Server weiterhin entgegen&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;max. Verbindungen&#039;&#039; und &#039;&#039;max. Anfragen&#039;&#039; sind die beiden&lt;br /&gt;
&#039;&#039;&#039;Lastgrenzen&#039;&#039;&#039; des Profils. Davon zu unterscheiden sind die festen Grenzen der&lt;br /&gt;
persistenten Verbindungen - 10 Sekunden Leerlauf, 100 Anfragen je Verbindung -,&lt;br /&gt;
die nicht der Last dienen, sondern der Hygiene und nicht einstellbar sind. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer|REST-Server]], Abschnitt Persistente&lt;br /&gt;
Verbindungen.}}&lt;br /&gt;
&lt;br /&gt;
Die Inhalte der Zertifikate werden direkt in OBS als Text gespeichert. Zur Laufzeit schreibt der Server die PEM-Inhalte für den Bruchteil einer Sekunde in ein Temp-Verzeichnis, damit OpenSSL sie beim Start lesen kann; danach werden die Temp-Dateien sofort wieder gelöscht.&lt;br /&gt;
&lt;br /&gt;
==Modi==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Modus               !! Voraussetzungen                       !! Anwendung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039;  || Zertifikat + Schlüssel                || Empfohlener Default für alle Server, die über das öffentliche Netz erreichbar sind&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;mTLS&#039;&#039;&#039;          || Zertifikat + Schlüssel + Root-Zert.   || Maschine-zu-Maschine-Kommunikation, in der jeder Client mit Cert validiert wird&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Plain-HTTP&#039;&#039;&#039;    || Haken &#039;&#039;kein SSL&#039;&#039;                    || Ausschliesslich lokale Tests. Erkennt TLS-Versuche auf dem Plain-Port und trennt sie direkt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beide Richtungen werden erkannt: Eine &#039;&#039;&#039;unverschlüsselte Anfrage an einen&lt;br /&gt;
TLS-Port&#039;&#039;&#039; (&#039;&#039;http&#039;&#039; statt &#039;&#039;https&#039;&#039;) beantwortet der Server mit &#039;&#039;&#039;HTTP 400&#039;&#039;&#039;&lt;br /&gt;
und einem Klartext-Hinweis. Die Abweisung erfolgt &#039;&#039;&#039;vor&#039;&#039;&#039; Authentifizierung,&lt;br /&gt;
Rate-Limit und Endpunkt-Skript - ein falsch konfigurierter Client löst also&lt;br /&gt;
keine Verarbeitung aus.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Bei Standard-TLS und mTLS fährt der Server ohne Zertifikat und Schlüssel gar nicht erst hoch. Bei aktiver Erneuerung trägt der Dienst beides selbst ein. Plain-HTTP umgeht TLS komplett und ist nicht für Produktivumgebungen geeignet.}}&lt;br /&gt;
&lt;br /&gt;
==TLS-Version, Cipher und Zertifikats-Rotation==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TLS-Version:&#039;&#039;&#039; Der Server erzwingt &#039;&#039;&#039;TLS 1.2&#039;&#039;&#039;. Die Cipher-Liste ist auf ECDHE-Schlüsselaustausch (Forward Secrecy) mit AEAD-Verfahren (AES-GCM, ChaCha20-Poly1305) beschränkt; CBC, statisches RSA-KEX und DHE sind ausgeschlossen.&lt;br /&gt;
* &#039;&#039;&#039;HSTS:&#039;&#039;&#039; Jede Antwort eines TLS-Profils trägt &#039;&#039;Strict-Transport-Security: max-age=31536000; includeSubDomains&#039;&#039;. Ein Browser spricht die Adresse damit ein Jahr lang nur noch über HTTPS an. Beim Profil &#039;&#039;Kein SSL&#039;&#039; wird der Header bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; gesetzt - der Browser würde ihn sonst speichern und die Erzwingung auch auf produktive Sitzungen anwenden.&lt;br /&gt;
* &#039;&#039;&#039;Rotation:&#039;&#039;&#039; Bei &#039;&#039;&#039;automatischer Erneuerung&#039;&#039;&#039; tauscht der Dienst das Zertifikat selbst und startet dabei nur das betroffene Server-Profil neu; die übrigen Profile und der Dienst laufen weiter. Bei manueller Pflege ändern Sie die PEM-Inhalte und starten den REST-Dienst neu - bis dahin läuft das alte Zertifikat weiter.&lt;br /&gt;
* &#039;&#039;&#039;Certificate-Pinning:&#039;&#039;&#039; Mobile Clients können den Server-Public-Key pinnen. Zertifikatswechsel daher &#039;&#039;&#039;rechtzeitig ankündigen&#039;&#039;&#039; und mit einem &#039;&#039;&#039;Backup-Pin&#039;&#039;&#039; vorbereiten - der neue Pin muss vor dem Wechsel in einer Client-Version ausgerollt sein, sonst sperrt sich die App aus.&lt;br /&gt;
==Automatische Zertifikatserneuerung==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server kann sein TLS-Zertifikat selbst bei einer Zertifizierungsstelle beantragen und vor Ablauf erneuern. Prüfarten, Voraussetzungen, Einrichtung, Ablauf und Konsolenbefehle beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]].&lt;br /&gt;
&lt;br /&gt;
===Felder===&lt;br /&gt;
&lt;br /&gt;
Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                       !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Automatische Erneuerung    || &#039;&#039;deaktiviert&#039;&#039; = aus. &#039;&#039;Produktivbetrieb&#039;&#039; = der Dienst beantragt und erneuert das Zertifikat selbst. &#039;&#039;Testbetrieb&#039;&#039; = nur für Testsysteme, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung#Testbetrieb|Testbetrieb]]&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikatsname (FQDN)     || Der Name, auf den das Zertifikat ausgestellt wird. Pflicht, sobald die Erneuerung aktiv ist&lt;br /&gt;
|-&lt;br /&gt;
| Kontakt bei der CA         || Optional. Die Zertifizierungsstelle schickt an diese Adresse Warnungen, wenn ein Zertifikat abzulaufen droht&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikat gültig bis      || Ablaufdatum des aktiven Zertifikats&lt;br /&gt;
|-&lt;br /&gt;
| Testzertifikat aus Staging || Gesetzt, wenn das aktive Zertifikat aus dem Testbetrieb stammt&lt;br /&gt;
|-&lt;br /&gt;
| Zustand                    || &#039;&#039;idle&#039;&#039; = nichts zu tun, &#039;&#039;pending&#039;&#039; = Vorgang läuft, &#039;&#039;error&#039;&#039; = letzter Versuch fehlgeschlagen&lt;br /&gt;
|-&lt;br /&gt;
| Letzte Meldung             || Ergebnis des letzten Vorgangs - im Störungsfall das erste Feld, in das Sie sehen&lt;br /&gt;
|-&lt;br /&gt;
| Gesperrt bis               || Nach einem Fehlschlag wartet der Dienst bis zu diesem Zeitpunkt, bevor er es erneut versucht&lt;br /&gt;
|-&lt;br /&gt;
| Letzter Selbsttest am      || Zeitpunkt der letzten Prüfung über das Testsystem der Zertifizierungsstelle&lt;br /&gt;
|-&lt;br /&gt;
| Selbsttest bestanden       || Ergebnis dieser Prüfung&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die Felder ab &#039;&#039;Zertifikat gültig bis&#039;&#039; füllt der Dienst selbst; sie dienen der Anzeige.&lt;br /&gt;
&lt;br /&gt;
Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                       !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Prüfart                    || &#039;&#039;http-01 (Port 80)&#039;&#039; oder &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039;, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung#Welche Prüfart?|Welche Prüfart?]]&lt;br /&gt;
|-&lt;br /&gt;
| DNS-Anbieter               || Nur dns-01: &#039;&#039;acme-dns&#039;&#039;, &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Adresse (acme-dns)         || Nur acme-dns: Adresse der Instanz, beginnt mit &amp;lt;code&amp;gt;https://&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Benutzer (acme-dns)        || Nur acme-dns: Benutzer des Kontos. Bleiben Benutzer, Schlüssel und Name leer, legt der Dienst das Konto selbst an&lt;br /&gt;
|-&lt;br /&gt;
| Schlüssel / API-Token      || acme-dns: Passwort des Kontos. Hetzner und Cloudflare: API-Token. IONOS: API-Schlüssel in der Form &amp;lt;code&amp;gt;Präfix.Secret&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Name des TXT-Eintrags      || Leer = &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Zertifikatsname&amp;gt;&amp;lt;/code&amp;gt;. Bei CNAME-Delegation das Ziel des CNAME, bei acme-dns die &#039;&#039;fulldomain&#039;&#039; des Kontos&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Die Zugangsdaten stehen im Klartext im Server-Profil. Verwenden Sie Token, die nur die nötige Zone ändern dürfen.}}&lt;br /&gt;
&lt;br /&gt;
==Rate-Limit je Profil==&lt;br /&gt;
&lt;br /&gt;
Jedes Server-Profil führt &#039;&#039;&#039;eigene Zähler&#039;&#039;&#039; und kann &#039;&#039;&#039;eigene Schwellen&#039;&#039;&#039; haben.&lt;br /&gt;
Ein Profil, das gerade überrannt wird, bremst damit die anderen Profile nicht mit&lt;br /&gt;
aus - das Public-Profil und das interne mTLS-Profil sind voneinander unabhängig.&lt;br /&gt;
&lt;br /&gt;
Gezählt wird pro Schlüssel, und der Schlüssel ist der &#039;&#039;&#039;API-Key&#039;&#039;&#039; des Zugangs.&lt;br /&gt;
Sendet ein Konsument keinen API-Key (Zugang mit &#039;&#039;*&#039;&#039;), wird ersatzweise auf die&lt;br /&gt;
&#039;&#039;&#039;IP-Adresse&#039;&#039;&#039; gezählt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Das ist der wichtigste Punkt bei mobilen Anwendungen: Sitzen alle&lt;br /&gt;
Benutzer hinter einer gemeinsamen Firmen-IP und sendet die App keinen eigenen&lt;br /&gt;
API-Key, teilen sich &#039;&#039;&#039;alle Benutzer einen Zähler&#039;&#039;&#039;. Mit den Standardwerten sind&lt;br /&gt;
das 10 Fehlversuche für den ganzen Betrieb pro Minute - nach ein paar falschen&lt;br /&gt;
Passworteingaben steht die Abteilung. Für solche Profile die Schwellen bewusst&lt;br /&gt;
höher setzen oder jedem Konsumenten einen eigenen API-Key geben.}}&lt;br /&gt;
&lt;br /&gt;
Dasselbe gilt hinter einem Reverse-Proxy: Alle Anfragen kommen dort von der IP-Adresse&lt;br /&gt;
des Proxys, und alle Konsumenten ohne API-Key teilen sich einen Zähler. Der Header&lt;br /&gt;
&#039;&#039;X-Forwarded-For&#039;&#039; ändert daran nichts, er wird nur protokolliert.&lt;br /&gt;
&lt;br /&gt;
Bleiben alle vier Felder leer (bzw. 0), gelten die Standardwerte. Die Werte werden&lt;br /&gt;
&#039;&#039;&#039;beim Start&#039;&#039;&#039; des REST-Dienstes gelesen - eine Änderung wird erst nach einem&lt;br /&gt;
Neustart wirksam.&lt;br /&gt;
&lt;br /&gt;
==Validierung beim Speichern==&lt;br /&gt;
&lt;br /&gt;
Immer geprüft wird:&lt;br /&gt;
&lt;br /&gt;
* Nr und Name sind vergeben, Nr liegt zwischen 1 und 99.&lt;br /&gt;
* Nr und Name sind eindeutig.&lt;br /&gt;
* Eingetragene Zertifikate und Schlüssel liegen im PEM-Format vor.&lt;br /&gt;
* Ein eingetragener Zertifikatsname enthält keine Adresse, keinen Port und keine Leerzeichen.&lt;br /&gt;
&lt;br /&gt;
Nur bei &#039;&#039;&#039;aktivem&#039;&#039;&#039; Profil zusätzlich:&lt;br /&gt;
&lt;br /&gt;
* Ohne &#039;&#039;kein SSL&#039;&#039; und ohne automatische Erneuerung sind Zertifikat und Schlüssel vorhanden.&lt;br /&gt;
* Bei &#039;&#039;mTLS-Modus aktiv&#039;&#039; ist das Root-Zertifikat vorhanden.&lt;br /&gt;
* Bei aktiver Erneuerung ist der Zertifikatsname vergeben, und &#039;&#039;kein SSL&#039;&#039; ist nicht gesetzt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein &#039;&#039;&#039;nicht aktives&#039;&#039;&#039; Profil dürfen Sie unvollständig speichern - es wird beim Start des Dienstes nicht geladen. Was Sie eintragen, wird trotzdem auf Plausibilität geprüft.}}&lt;br /&gt;
&lt;br /&gt;
==Bindungen==&lt;br /&gt;
&lt;br /&gt;
Eine Bindung ist die Kombination aus IP-Adresse und Port, auf der ein Server-Profil lauscht. Pro Server-Profil können mehrere Bindungen existieren, z.B. um die gleiche TLS-Konfiguration parallel auf 443 und 8443 anzubieten.&lt;br /&gt;
&lt;br /&gt;
===Aufruf===&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste den gewünschten Server markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die zum Server gehörende Bindungs-Liste.&lt;br /&gt;
&lt;br /&gt;
===Felder===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld    !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Host    || IP-Adresse oder Hostname, auf dem gelauscht wird. &#039;&#039;0.0.0.0&#039;&#039; = alle IPv4-Adressen; &#039;&#039;127.0.0.1&#039;&#039; = nur lokal; explizite IPs binden gezielt auf eine Netzwerkkarte&lt;br /&gt;
|-&lt;br /&gt;
| Port    || TCP-Port (1-65535). 443/8443 für HTTPS, frei wählbar für Debug-Profile&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv   || Bindung wird beim Server-Start geladen&lt;br /&gt;
|-&lt;br /&gt;
| Standard|| Markiert die Standard-Bindung des Servers. Pro Server-Profil ist nur eine Standard-Bindung erlaubt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Wirksamkeit===&lt;br /&gt;
&lt;br /&gt;
Bindungen werden ausschliesslich beim Start des REST-Dienstes geladen. Änderungen werden daher erst nach einem Neustart aktiv. Bis dahin laufen die alten Bindungen weiter.&lt;br /&gt;
&lt;br /&gt;
==Listen-Funktionen==&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfg&#039;&#039;&#039; - neues Server-Profil&lt;br /&gt;
* &#039;&#039;&#039;Return&#039;&#039;&#039; - vorhandenes Profil bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F4&#039;&#039;&#039; - Sortierung wählen&lt;br /&gt;
* &#039;&#039;&#039;F6&#039;&#039;&#039; - Bindungs-Liste des markierten Servers&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/%C3%96ffentliche_Freigabe&amp;diff=67061</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/%C3%96ffentliche_Freigabe&amp;diff=67061"/>
		<updated>2026-10-26T09:31:23Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Neue Seite: Varianten der öffentlichen Freigabe (Portfreigabe, Reverse-Proxy, VPN) als Entscheidungshilfe [Styleguide-Abweichung: Vergleichstabelle statt Seitenliste]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
{{Seitentyp|Übersicht}}&lt;br /&gt;
= Öffentliche Freigabe =&lt;br /&gt;
&lt;br /&gt;
Apps auf mobilen Geräten und externe Systeme erreichen den REST-Server nur, wenn er aus dem Internet erreichbar ist. Diese Seite stellt die drei Wege dafür gegenüber und richtet sich an die IT-Administration, die über die Freigabe entscheidet.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Läuft der REST-Server auf dem OBS-Server, liegt dort auch die Datenbank. Die Varianten unterscheiden sich vor allem darin, ob eine Verbindung aus dem Internet diesen Server direkt erreicht.}}&lt;br /&gt;
&lt;br /&gt;
== Varianten im Vergleich ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Merkmal !! Direkte Portfreigabe !! Reverse-Proxy !! VPN&lt;br /&gt;
|-&lt;br /&gt;
| Aus dem Internet erreichbar || OBS-Server: Port des REST-Servers, bei Prüfart http-01 zusätzlich Port 80 || nur der Proxy (Port 443) || nur das VPN-Gateway&lt;br /&gt;
|-&lt;br /&gt;
| Aufwand für die Nutzer der App || keiner || keiner || VPN-Client einrichten und vor der Nutzung einschalten&lt;br /&gt;
|-&lt;br /&gt;
| Aufwand für die IT || Portweiterleitung am Router || Proxy betreiben, Zertifikat am Proxy || VPN-Zugang für jedes Gerät&lt;br /&gt;
|-&lt;br /&gt;
| Filtern und Protokollieren vor dem OBS-Server || – || ✓ || nur auf Netzebene&lt;br /&gt;
|-&lt;br /&gt;
| Öffentliches Zertifikat || im REST-Server, Prüfart http-01 oder dns-01 || am Proxy || im REST-Server, Prüfart dns-01&lt;br /&gt;
|-&lt;br /&gt;
| REST-Server sieht die IP-Adresse des Geräts || ✓ || nur im Protokoll (über &#039;&#039;X-Forwarded-For&#039;&#039;) || ✓, sofern das VPN-Gateway die Adressen nicht umsetzt (NAT)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Direkte Portfreigabe ==&lt;br /&gt;
Der Router leitet den Port des REST-Servers aus dem Internet auf den OBS-Server weiter. Für die Zertifikatserneuerung mit der Prüfart http-01 wird zusätzlich Port 80 weitergeleitet.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Die einfachste Einrichtung, es wird keine weitere Komponente gebraucht.&lt;br /&gt;
* Die App funktioniert überall, ohne dass die Nutzer etwas tun müssen.&lt;br /&gt;
* Der REST-Server sieht die IP-Adresse jedes Geräts. Host-Beschränkung, Rate-Limit und Protokoll arbeiten damit je Gerät.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Der OBS-Server mit der Datenbank ist direkt aus dem Internet erreichbar. Schutz bieten allein TLS, die Zugänge (API-Key, JWT) und die Grenzen des REST-Servers.&lt;br /&gt;
* Mit der Prüfart http-01 ist zusätzlich Port 80 weitergeleitet. Mit der Prüfart dns-01 entfällt das.&lt;br /&gt;
&lt;br /&gt;
== Reverse-Proxy ==&lt;br /&gt;
Ein Reverse-Proxy, zum Beispiel in einer DMZ oder als Funktion der Firewall, nimmt HTTPS auf Port 443 an. Er prüft die Anfrage und reicht sie über eine eigene, ebenfalls verschlüsselte Verbindung an den REST-Server weiter.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Der OBS-Server ist aus dem Internet nicht erreichbar, nur der Proxy.&lt;br /&gt;
* Am Proxy lassen sich Anfragen nach Ihren Vorgaben filtern, drosseln und protokollieren.&lt;br /&gt;
* Für die Nutzer der App ändert sich nichts.&lt;br /&gt;
* Das öffentliche Zertifikat liegt am Proxy. Die Zertifikatserneuerung des REST-Servers wird nicht gebraucht.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Der Proxy muss betrieben und gepflegt werden.&lt;br /&gt;
* Der REST-Server sieht als Absender nur den Proxy. Die Host-Beschränkung eines Zugangs kann keine Geräte unterscheiden, und Anfragen ohne API-Key teilen sich einen Zähler des Rate-Limits. Die IP-Adresse des Geräts steht nur im Protokoll, wenn der Proxy den Header &#039;&#039;X-Forwarded-For&#039;&#039; setzt.&lt;br /&gt;
&lt;br /&gt;
Die Einrichtung beschreibt [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung#Betrieb hinter einem Reverse-Proxy|Betrieb hinter einem Reverse-Proxy]].&lt;br /&gt;
&lt;br /&gt;
== VPN ==&lt;br /&gt;
Die Geräte verbinden sich per VPN mit dem Netz, in dem der OBS-Server steht. Die App spricht durch den Tunnel per HTTPS mit dem REST-Server. Aus dem Internet ist nur das VPN-Gateway erreichbar.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Vorteile&#039;&#039;&#039;&lt;br /&gt;
* Für den REST-Server ist kein Port aus dem Internet offen.&lt;br /&gt;
* Nur Geräte mit VPN-Zugang erreichen den REST-Server.&lt;br /&gt;
* Setzt das VPN-Gateway die Adressen nicht um, sieht der REST-Server die VPN-Adresse jedes Geräts.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Nachteile&#039;&#039;&#039;&lt;br /&gt;
* Jedes Gerät braucht einen VPN-Client und einen eigenen Zugang. Ohne zentrale Geräteverwaltung schalten die Nutzer das VPN von Hand ein. Ist es aus, meldet die App, dass der Server nicht erreichbar ist.&lt;br /&gt;
* Ein VPN-Zugang öffnet den Weg ins Netz. Beschränken Sie ihn auf die Adresse und den Port des REST-Servers.&lt;br /&gt;
* Das Zertifikat lässt sich nur mit der Prüfart dns-01 erneuern, weil die Zertifizierungsstelle den Server nicht erreicht.&lt;br /&gt;
* Der Name des REST-Servers muss auf dem Gerät auf die interne Adresse zeigen. Das geht über einen DNS-Server, den das VPN mitliefert, oder über einen öffentlichen DNS-Eintrag mit der internen Adresse. Manche Router verwerfen solche Antworten als Schutz gegen DNS-Rebinding; dann löst der Name in diesem Netz nicht auf.&lt;br /&gt;
&lt;br /&gt;
Das Zertifikat richten Sie wie unter [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]] beschrieben mit der Prüfart dns-01 ein.&lt;br /&gt;
&lt;br /&gt;
== Entscheidungshilfe ==&lt;br /&gt;
* &#039;&#039;&#039;Keine eingehende Verbindung zum OBS-Server erlaubt, DMZ oder Firewall mit Proxy-Funktion vorhanden:&#039;&#039;&#039; Reverse-Proxy.&lt;br /&gt;
* &#039;&#039;&#039;Geräte werden zentral verwaltet oder ein VPN ist ohnehin im Einsatz:&#039;&#039;&#039; VPN.&lt;br /&gt;
* &#039;&#039;&#039;Kleine Installation ohne Vorgaben der IT:&#039;&#039;&#039; Direkte Portfreigabe, möglichst mit der Prüfart dns-01, damit Port 80 geschlossen bleibt.&lt;br /&gt;
* &#039;&#039;&#039;Für alle Varianten:&#039;&#039;&#039; Wählen Sie nach Möglichkeit Port 443. Manche Mobilfunk- und Gästenetze sperren ungewöhnliche Ports.&lt;br /&gt;
&lt;br /&gt;
== Siehe auch ==&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung|Zertifikatserneuerung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge#Host-Beschränkung|Host-Beschränkung]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zertifikatserneuerung&amp;diff=67060</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Zertifikatserneuerung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zertifikatserneuerung&amp;diff=67060"/>
		<updated>2026-10-26T09:31:23Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Verweise auf Öffentliche Freigabe ergänzt&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Vorlage:Kostenpflichtige Module}}&lt;br /&gt;
{{Seitentyp|Anleitung}}&lt;br /&gt;
= Zertifikatserneuerung =&lt;br /&gt;
&lt;br /&gt;
Der REST-Server kann sein TLS-Zertifikat selbst bei der Zertifizierungsstelle Let&#039;s Encrypt beantragen und vor Ablauf erneuern. Zertifikat und privater Schlüssel entstehen auf Ihrem Server und verlassen ihn nicht. Diese Seite beschreibt, wie Sie die Erneuerung je nach Netzaufbau einrichten.&lt;br /&gt;
&lt;br /&gt;
{{Auf einen Blick|1=&lt;br /&gt;
* &#039;&#039;&#039;Zwei Prüfarten.&#039;&#039;&#039; http-01 prüft über Port 80, dns-01 über einen DNS-Eintrag und braucht keinen offenen Port.&lt;br /&gt;
* &#039;&#039;&#039;Selbsttest vorab.&#039;&#039;&#039; Vor dem ersten Zertifikat und danach wöchentlich prüft der Dienst den Weg über das Testsystem der Zertifizierungsstelle.&lt;br /&gt;
* &#039;&#039;&#039;Erneuerung ab 30 Tagen Restlaufzeit.&#039;&#039;&#039; Das bisherige Zertifikat bleibt als Rückfall erhalten.&lt;br /&gt;
* &#039;&#039;&#039;dns-01 braucht nur ausgehende Verbindungen&#039;&#039;&#039; und eignet sich damit für VPN und für Server ohne Portfreigabe.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Voraussetzungen ==&lt;br /&gt;
* Ein Name, auf den das Zertifikat lautet, zum Beispiel &#039;&#039;obs.kunde.de&#039;&#039;. Platzhalter-Zertifikate (&#039;&#039;*.kunde.de&#039;&#039;) werden nicht unterstützt.&lt;br /&gt;
* Ein Server-Profil mit TLS, also ohne Haken &#039;&#039;&#039;kein SSL&#039;&#039;&#039;.&lt;br /&gt;
* Ausgehende HTTPS-Verbindungen zur Zertifizierungsstelle (&amp;lt;code&amp;gt;acme-v02.api.letsencrypt.org&amp;lt;/code&amp;gt; und für den Selbsttest &amp;lt;code&amp;gt;acme-staging-v02.api.letsencrypt.org&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Bei dns-01 zusätzlich ausgehende HTTPS-Verbindungen zur Schnittstelle des DNS-Anbieters bzw. zur acme-dns-Instanz.&lt;br /&gt;
&lt;br /&gt;
=== Welche Prüfart? ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Prüfart !! Funktioniert, wenn !! Aus dem Internet erreichbar sein muss&lt;br /&gt;
|-&lt;br /&gt;
| http-01 || der Name öffentlich auf diesen Server zeigt und Port 80 von aussen auf ihn weitergeleitet ist || Port 80&lt;br /&gt;
|-&lt;br /&gt;
| dns-01 || sich der DNS-Eintrag &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; über eine Schnittstelle schreiben lässt, direkt oder über einen CNAME || nichts&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Welche Prüfart zu Ihrer [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|öffentlichen Freigabe]] passt:&lt;br /&gt;
* &#039;&#039;&#039;Direkte Portfreigabe:&#039;&#039;&#039; http-01 oder dns-01. Mit dns-01 bleibt Port 80 geschlossen.&lt;br /&gt;
* &#039;&#039;&#039;Reverse-Proxy:&#039;&#039;&#039; Das öffentliche Zertifikat liegt am Proxy, die Erneuerung im REST-Server wird nicht gebraucht.&lt;br /&gt;
* &#039;&#039;&#039;VPN:&#039;&#039;&#039; dns-01.&lt;br /&gt;
&lt;br /&gt;
=== Wege bei dns-01 ===&lt;br /&gt;
Bei dns-01 fragt die Zertifizierungsstelle den TXT-Eintrag &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; ab. Der Dienst schreibt ihn vor der Prüfung und löscht ihn danach wieder. Dafür gibt es drei Wege:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Weg !! Wann !! Einmalig beim DNS des Namens !! Einstellung &#039;&#039;&#039;DNS-Anbieter&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Direkt beim Anbieter || Die Zone des Namens liegt bei Hetzner, Cloudflare oder IONOS || nichts, Sie brauchen nur ein API-Token || &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| CNAME auf eine Zone bei einem Anbieter || Die Zone des Namens liegt bei einem anderen Anbieter, oder im REST-Server sollen keine Zugangsdaten zur eigenen Zone liegen || CNAME &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf einen Namen in einer Zone bei Hetzner, Cloudflare oder IONOS || &#039;&#039;Hetzner&#039;&#039;, &#039;&#039;Cloudflare&#039;&#039; oder &#039;&#039;IONOS&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| CNAME auf acme-dns || Eine acme-dns-Instanz steht zur Verfügung, im eigenen Betrieb oder bei einem Dienstleister || CNAME &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf die &#039;&#039;fulldomain&#039;&#039; des acme-dns-Kontos || &#039;&#039;acme-dns&#039;&#039;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim CNAME-Weg genügt beim DNS-Anbieter des Namens ein von Hand angelegter Eintrag, auch bei Anbietern ohne Schnittstelle. Der REST-Server bekommt dann nur Zugang zur Zone, auf die der CNAME zeigt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Namen von DynDNS-Diensten (zum Beispiel &#039;&#039;…myfritz.net&#039;&#039;) eignen sich in der Regel nicht als Zertifikatsname, weil sich dort keine eigenen Einträge anlegen lassen. Verwenden Sie einen Namen unter Ihrer eigenen Domain. Der DynDNS-Name bleibt davon unberührt, zum Beispiel für den Zugang zum VPN.}}&lt;br /&gt;
&lt;br /&gt;
== Vorgehen ==&lt;br /&gt;
=== Gemeinsame Einstellungen ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Wählen Sie &#039;&#039;&#039;Stammdaten → Z Weitere Stammdaten → REST-Server → Server&#039;&#039;&#039; und öffnen Sie das Server-Profil.&lt;br /&gt;
# Stellen Sie auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039;.&lt;br /&gt;
# Tragen Sie im Feld &#039;&#039;&#039;Zertifikatsname (FQDN)&#039;&#039;&#039; den Namen ein, auf den das Zertifikat lauten soll.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Kontakt bei der CA&#039;&#039;&#039; eine E-Mail-Adresse ein. Die Zertifizierungsstelle schickt dorthin Warnungen, wenn ein Zertifikat abzulaufen droht.&lt;br /&gt;
# Richten Sie die Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; nach einem der folgenden Abschnitte ein und speichern Sie.&lt;br /&gt;
# Starten Sie den REST-Dienst neu.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart http-01 ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Lassen Sie den Namen öffentlich auf die Adresse Ihres Internetanschlusses zeigen.&lt;br /&gt;
# Leiten Sie am Router Port 80 von aussen auf den OBS-Server weiter.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; im Feld &#039;&#039;&#039;Prüfart&#039;&#039;&#039; den Eintrag &#039;&#039;http-01 (Port 80)&#039;&#039;. Die übrigen Felder bleiben leer.&lt;br /&gt;
# Geben Sie in der Konsole des REST-Servers &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
Port 80 darf auf dem OBS-Server von keinem anderen Dienst belegt sein, häufig ist das ein Webserver wie IIS. Der REST-Server öffnet den Port nur für die Dauer der Prüfung und schliesst ihn danach wieder.&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 direkt beim DNS-Anbieter ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Erstellen Sie beim Anbieter der Zone ein API-Token:&lt;br /&gt;
#* &#039;&#039;&#039;Hetzner:&#039;&#039;&#039; ein API-Token des Projekts mit Lese- und Schreibrecht in der Hetzner Console. Zonen in der früheren DNS-Konsole (dns.hetzner.com) werden nicht unterstützt.&lt;br /&gt;
#* &#039;&#039;&#039;Cloudflare:&#039;&#039;&#039; ein API-Token mit der Berechtigung, DNS zu bearbeiten, beschränkt auf die Zone.&lt;br /&gt;
#* &#039;&#039;&#039;IONOS:&#039;&#039;&#039; einen API-Schlüssel. Er besteht aus Präfix und Secret.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; im Feld &#039;&#039;&#039;Prüfart&#039;&#039;&#039; den Eintrag &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und im Feld &#039;&#039;&#039;DNS-Anbieter&#039;&#039;&#039; den Anbieter.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; das Token ein, bei IONOS in der Form &amp;lt;code&amp;gt;Präfix.Secret&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Lassen Sie &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; leer.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 mit CNAME auf eine Zone bei einem DNS-Anbieter ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Legen Sie in einer Zone bei Hetzner, Cloudflare oder IONOS einen Namen für die Prüfung fest, zum Beispiel &#039;&#039;obs-kunde.acme.example.de&#039;&#039;. Einen Eintrag müssen Sie dort nicht anlegen.&lt;br /&gt;
# Legen Sie beim DNS-Anbieter Ihres Namens einen CNAME an: &amp;lt;code&amp;gt;_acme-challenge.obs.kunde.de&amp;lt;/code&amp;gt; auf &#039;&#039;obs-kunde.acme.example.de&#039;&#039;.&lt;br /&gt;
# Erstellen Sie ein API-Token für die Zone, in der das Ziel liegt, wie im vorigen Abschnitt beschrieben.&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; die Prüfart &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und den Anbieter dieser Zone.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; das Token ein.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; das Ziel des CNAME ein, hier &#039;&#039;obs-kunde.acme.example.de&#039;&#039;.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== Prüfart dns-01 mit CNAME auf acme-dns ===&lt;br /&gt;
{{Schritte|1=&lt;br /&gt;
# Wählen Sie auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; die Prüfart &#039;&#039;dns-01 (DNS-Eintrag)&#039;&#039; und den DNS-Anbieter &#039;&#039;acme-dns&#039;&#039;.&lt;br /&gt;
# Tragen Sie unter &#039;&#039;&#039;Adresse (acme-dns)&#039;&#039;&#039; die Adresse der Instanz ein. Sie beginnt mit &amp;lt;code&amp;gt;https://&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Lassen Sie &#039;&#039;&#039;Benutzer (acme-dns)&#039;&#039;&#039;, &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; und &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039; leer und speichern Sie.&lt;br /&gt;
# Geben Sie in der Konsole &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein. Der Dienst legt das Konto bei acme-dns an und trägt Benutzer, Schlüssel und Name selbst ein. Unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; steht danach der CNAME, den Sie anlegen müssen.&lt;br /&gt;
# Legen Sie beim DNS-Anbieter Ihres Namens diesen CNAME an: &amp;lt;code&amp;gt;_acme-challenge.&amp;lt;Name&amp;gt;&amp;lt;/code&amp;gt; auf die genannte &#039;&#039;fulldomain&#039;&#039;.&lt;br /&gt;
# Geben Sie in der Konsole erneut &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein.&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Leeren Sie die drei Felder später nicht. Der Dienst würde ein neues Konto mit einer neuen &#039;&#039;fulldomain&#039;&#039; anlegen, und der gesetzte CNAME zeigte ins Leere. Ein bestehendes Konto tragen Sie mit allen drei Angaben ein. Teilweise gefüllte Angaben weist die Maske beim Speichern ab.}}&lt;br /&gt;
&lt;br /&gt;
=== Erstes Zertifikat holen ===&lt;br /&gt;
Ist der Selbsttest bestanden, holt der Dienst das Zertifikat bei der nächsten stündlichen Prüfung. Sofort geht es mit &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; in der Konsole. Bis das erste Zertifikat da ist, kann sich kein Client mit dem Server-Profil verbinden.&lt;br /&gt;
&lt;br /&gt;
=== Testbetrieb ===&lt;br /&gt;
{{Achtung|Im &#039;&#039;&#039;Testbetrieb&#039;&#039;&#039; beantragt der Dienst das Zertifikat beim Testsystem der Zertifizierungsstelle. Einem solchen Zertifikat vertraut &#039;&#039;&#039;kein&#039;&#039;&#039; Client, Anwendungen können sich danach nicht mehr mit diesem Server verbinden. Verwenden Sie den Testbetrieb ausschliesslich auf Testsystemen. Zurück in den Produktivbetrieb kommen Sie, indem Sie das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039; stellen. Der Dienst holt daraufhin sofort ein gültiges Zertifikat, auch wenn das Testzertifikat noch lange läuft.}}&lt;br /&gt;
&lt;br /&gt;
== Ergebnis ==&lt;br /&gt;
Nach bestandenem Selbsttest ist auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; der Haken &#039;&#039;&#039;Selbsttest bestanden&#039;&#039;&#039; gesetzt. Unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; steht &amp;quot;Selbsttest bestanden - von aussen erreichbar&amp;quot; (http-01) bzw. &amp;quot;Selbsttest bestanden - DNS-Eintrag von der CA geprueft&amp;quot; (dns-01). Nach der Ausstellung zeigt &#039;&#039;&#039;Zertifikat gültig bis&#039;&#039;&#039; das Ablaufdatum, und &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; in der Konsole zeigt den Stand je Profil.&lt;br /&gt;
&lt;br /&gt;
Danach läuft die Erneuerung selbständig:&lt;br /&gt;
# &#039;&#039;&#039;Prüfung:&#039;&#039;&#039; Der Dienst sieht stündlich nach, ob etwas ansteht.&lt;br /&gt;
# &#039;&#039;&#039;Selbsttest:&#039;&#039;&#039; Einmal wöchentlich und nach jeder Änderung von Zertifikatsname, Prüfart, DNS-Anbieter oder Name des TXT-Eintrags prüft der Dienst den Weg über das Testsystem der Zertifizierungsstelle. Besteht der Test nicht, beantragt er kein Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Erneuerung:&#039;&#039;&#039; Ab 30 Tagen Restlaufzeit beantragt der Dienst ein neues Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Übernahme:&#039;&#039;&#039; Der Dienst prüft das neue Zertifikat, speichert es und bewahrt das bisherige als Rückfall auf. Anschliessend startet nur das betroffene Server-Profil neu.&lt;br /&gt;
# &#039;&#039;&#039;Rückfall:&#039;&#039;&#039; Startet das Profil mit dem neuen Zertifikat nicht, aktiviert der Dienst wieder das vorherige.&lt;br /&gt;
&lt;br /&gt;
Nach einem Fehlschlag wartet der Dienst zunehmend länger, bevor er es erneut versucht: eine, zwei, vier, acht und höchstens 24 Stunden. Das Feld &#039;&#039;&#039;Gesperrt bis&#039;&#039;&#039; zeigt, bis wann. &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; in der Konsole wirken sofort, auch während der Sperre.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; || Zeigt je Profil Betriebsart, Ablaufdatum, Zustand und das Ergebnis des letzten Selbsttests&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; || Prüft sofort den Weg über das Testsystem der Zertifizierungsstelle. Beantragt kein Zertifikat&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; || Stösst die Erneuerung sofort an, auch wenn sie noch nicht fällig ist&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Bei dns-01 wartet der Dienst nach dem Schreiben des Eintrags 30 Sekunden und dann bis zu drei Minuten, bis der Eintrag über öffentliche DNS-Server sichtbar ist. Erst dann löst er die Prüfung aus.&lt;br /&gt;
&lt;br /&gt;
== Wenn es nicht klappt ==&lt;br /&gt;
Die Meldungen stehen unter &#039;&#039;&#039;Letzte Meldung&#039;&#039;&#039; und im Protokoll des REST-Servers.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Meldung !! Ursache&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Port 80 laesst sich nicht oeffnen - er ist belegt&amp;quot; || Ein anderer Dienst auf dem OBS-Server belegt Port 80, häufig IIS. Dienst abschalten oder die Prüfart dns-01 verwenden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;dns-01 ist eingestellt, aber kein DNS-Anbieter&amp;quot; || Auf der Registerkarte &#039;&#039;&#039;5 DNS-Prüfung&#039;&#039;&#039; ist kein DNS-Anbieter gewählt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;kein API-Token&amp;quot; || Das Feld &#039;&#039;&#039;Schlüssel / API-Token&#039;&#039;&#039; ist leer&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;acme-dns: Konto unvollstaendig&amp;quot; || Von Benutzer, Schlüssel und Name des TXT-Eintrags ist nur ein Teil gefüllt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Hetzner: keine Zone zu … im Projekt des Tokens&amp;quot; || Das Token gehört zu einem anderen Projekt, oder die Zone liegt noch in der früheren DNS-Konsole&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;Cloudflare: keine Zone zu …, die das Token sehen darf&amp;quot; || Das Token ist nicht für diese Zone freigegeben&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;IONOS: keine Zone zu … im Vertrag des Schluessels&amp;quot; || Der API-Schlüssel gehört zu einem anderen Vertrag&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;… lehnt den TXT-Eintrag ab (HTTP 401)&amp;quot; oder &amp;quot;(HTTP 403)&amp;quot; || Das Token ist ungültig oder darf keine DNS-Einträge ändern&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;… ist nach 210 Sekunden noch nicht ueberall sichtbar&amp;quot; || Der Eintrag ist über öffentliche DNS-Server nicht zu sehen. Meist fehlt der CNAME oder zeigt auf ein anderes Ziel als &#039;&#039;&#039;Name des TXT-Eintrags&#039;&#039;&#039;. Der Dienst löst die Prüfung trotzdem aus&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;kein oeffentlicher DNS-Server erreichbar&amp;quot; || Ausgehendes DNS (Port 53) ist gesperrt. Der Dienst kann die Sichtbarkeit nicht prüfen und wartet fest 60 Sekunden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;quot;acme-dns-Konto angelegt - jetzt CNAME … setzen&amp;quot; || Kein Fehler: Legen Sie den genannten CNAME an und geben Sie &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; ein&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Siehe auch ==&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Öffentliche Freigabe|Öffentliche Freigabe]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile#Automatische Zertifikatserneuerung|Server-Profile: Felder der Zertifikatserneuerung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=67058</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Einrichtung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=67058"/>
		<updated>2026-10-26T08:22:19Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Abschnitt Betrieb hinter einem Reverse-Proxy ergänzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Einrichtung eines REST-Endpunktes=&lt;br /&gt;
&lt;br /&gt;
Die folgende Reihenfolge stellt sicher, dass ein lauffähiger Endpunkt mit möglichst wenigen Korrektur-Schleifen entsteht.&lt;br /&gt;
&lt;br /&gt;
==Vorbereitung==&lt;br /&gt;
&lt;br /&gt;
# OBS-Support kontaktieren und das REST-Server-Modul aktivieren lassen.&lt;br /&gt;
# Prüfen, ob auf dem Zielrechner (OBS-Server oder dedizierter REST-Server) Port und IP-Adresse frei sind.&lt;br /&gt;
# Bei TLS-Profilen: TLS-Zertifikat (PEM, mit Kette), privaten Schlüssel (PEM) und - falls mTLS - das CA-Root-Zertifikat bereithalten. Alternativ nutzen Sie die automatische Zertifikatserneuerung; dann genügen ein öffentlicher Name und eine Weiterleitung von Port 80 auf diesen Server. Soll der Server nicht direkt aus dem Internet erreichbar sein, siehe [[#Betrieb hinter einem Reverse-Proxy|Betrieb hinter einem Reverse-Proxy]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 1: Server-Profil anlegen==&lt;br /&gt;
&lt;br /&gt;
In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server&#039;&#039;&#039; den Punkt &#039;&#039;&#039;Server&#039;&#039;&#039; öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; ein neues Profil anlegen. Pflichtfelder:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Nr&#039;&#039;&#039; - eindeutige Nummer (1-99), wird beim Anlegen automatisch vorgeschlagen.&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - sprechender Name (z.B. &#039;&#039;Public-API&#039;&#039;, &#039;&#039;Internal-mTLS&#039;&#039;, &#039;&#039;Debug-Local&#039;&#039;).&lt;br /&gt;
* Auswahl des Modus:&lt;br /&gt;
** &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039; (Default): Zertifikat und Schlüssel hinterlegen - oder auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; die automatische Erneuerung aktivieren.&lt;br /&gt;
** &#039;&#039;&#039;mTLS-Modus aktiv&#039;&#039;&#039; setzen: zusätzlich das CA-Root-Zertifikat hinterlegen, gegen das die Client-Zertifikate geprüft werden. Jeder Client muss dann ein gültiges Zertifikat vorlegen.&lt;br /&gt;
** &#039;&#039;&#039;kein SSL&#039;&#039;&#039; setzen: ausschliesslich für lokale Tests, der Server akzeptiert dann nur Plain-HTTP.&lt;br /&gt;
&lt;br /&gt;
Optional lassen sich am Profil eigene &#039;&#039;&#039;Rate-Limit-Schwellen&#039;&#039;&#039; hinterlegen. Ohne&lt;br /&gt;
Angabe gelten die Standardwerte (60-Sekunden-Fenster, 120 erfolgreiche und 10&lt;br /&gt;
fehlgeschlagene Anfragen je Schlüssel, 600 Anfragen je Profil). Für Anwendungen&lt;br /&gt;
ohne eigenen API-Key - typisch mobile Apps - sollten die Werte bewusst gesetzt&lt;br /&gt;
werden, weil dann alle Benutzer hinter einer IP auf denselben Zähler laufen.&lt;br /&gt;
&lt;br /&gt;
Details siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 2: Bindung anlegen==&lt;br /&gt;
&lt;br /&gt;
Im Server-Profil mit &#039;&#039;&#039;F6&#039;&#039;&#039; die Bindungs-Liste öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; eine neue Bindung anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - IP-Adresse, auf der gelauscht werden soll (z.B. &#039;&#039;0.0.0.0&#039;&#039; für alle Adressen, &#039;&#039;127.0.0.1&#039;&#039; für lokal).&lt;br /&gt;
* &#039;&#039;&#039;Port&#039;&#039;&#039; - Port-Nummer (typisch 443 für HTTPS, 8099 für Debug, ggf. abweichend). {{Achtung|Port 8080 wird für RustDesk verwendet und darf daher nicht verwendet werden}}&lt;br /&gt;
* &#039;&#039;&#039;Standard&#039;&#039;&#039; setzen, wenn diese Bindung die Standard-Bindung des Servers ist (pro Server-Profil nur eine).&lt;br /&gt;
&lt;br /&gt;
==Schritt 3: REST-Dienst starten / neustarten==&lt;br /&gt;
&lt;br /&gt;
Damit Änderungen an Server-Profilen oder Bindungen wirksam werden, muss der OBS REST-Server-Dienst (oder die Konsole) gestartet bzw. neugestartet werden. Das Profil wird beim Start aus der Datenbank gelesen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 4: Zugang anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Zugänge&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Zugang anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - erscheint im Protokoll und in der Statistik.&lt;br /&gt;
* &#039;&#039;&#039;API-Key&#039;&#039;&#039; - eindeutig, mit dem Schlüsselsymbol kann ein zufälliger Key generiert werden.&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - optional die IP oder der Hostname des Konsumenten (sollte, wenn möglich, immer gesetzt werden).&lt;br /&gt;
* Optional: &#039;&#039;&#039;CORS-Origins&#039;&#039;&#039; und &#039;&#039;&#039;JWT-Konfiguration&#039;&#039;&#039;, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 5: Endpunkt anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Endpunkte&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Endpunkt anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Endpunkt&#039;&#039;&#039; - Ressourcen-/Anzeigename (z.B. &#039;&#039;orders&#039;&#039;, &#039;&#039;kalender&#039;&#039;); dient nur der Übersicht, nicht dem Routing.&lt;br /&gt;
* &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; - der maßgebliche Routing-Pfad, ggf. mit Platzhaltern (z.B. &#039;&#039;/orders&#039;&#039;, &#039;&#039;/orders/{uid}&#039;&#039;, &#039;&#039;/orders/{uid}/modules/{code}&#039;&#039;).&lt;br /&gt;
* &#039;&#039;&#039;Server&#039;&#039;&#039; - die Server-Auswahl bestimmt, über welches Server-Profil der Endpunkt erreichbar ist.&lt;br /&gt;
* &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; - nur setzen, wenn der Endpunkt Dateien entgegennehmen soll; dann auch die &#039;&#039;&#039;Max. Upload-Grösse&#039;&#039;&#039; in MB festlegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6]]).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Eine Versionskennung wird als statisches Segment ins Pfad-Template aufgenommen (z.B. &#039;&#039;/orders/v1&#039;&#039;); ein eigenes Versionsfeld gibt es nicht. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].}}&lt;br /&gt;
&lt;br /&gt;
Anschliessend mit &#039;&#039;&#039;F7&#039;&#039;&#039; das Endpunkt-Skript pflegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]]).&lt;br /&gt;
&lt;br /&gt;
==Schritt 6: Zugang für den Endpunkt freischalten==&lt;br /&gt;
&lt;br /&gt;
Aus der Endpunkt-Liste den neuen Endpunkt markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die Berechtigungs-Liste. Mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; kann ein oder mehrere Zugänge ausgewählt und mit &#039;&#039;&#039;F2&#039;&#039;&#039; übernommen werden. Erst nach dieser Freischaltung darf ein Zugang den Endpunkt aufrufen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 7: Funktionsprüfung==&lt;br /&gt;
&lt;br /&gt;
Aufruf mit einem HTTP-Client (curl, Postman, Browser-Plug-in):&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; https://api.meinserver.de/[Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Bei korrektem Setup erscheint die JSON-Antwort des Skripts. Fehlerfälle werden in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; protokolliert.&lt;br /&gt;
&lt;br /&gt;
==Schritt 8: Statistik und Protokoll prüfen==&lt;br /&gt;
&lt;br /&gt;
* In der Endpunkt-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Endpunkts.&lt;br /&gt;
* In der Zugänge-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Zugangs.&lt;br /&gt;
* In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Protokoll&#039;&#039;&#039; die Ereignisse prüfen.&lt;br /&gt;
&lt;br /&gt;
==Betrieb hinter einem Reverse-Proxy==&lt;br /&gt;
&lt;br /&gt;
Läuft der REST-Server auf dem OBS-Server, soll dieser Server meist nicht direkt aus dem Internet erreichbar sein. Dann nimmt ein Reverse-Proxy die Anfragen entgegen, zum Beispiel in einer DMZ oder als Funktion der Firewall, und reicht sie an den REST-Server weiter. Von aussen ist nur der Proxy erreichbar.&lt;br /&gt;
&lt;br /&gt;
Am Proxy und im Netz richten Sie ein:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Öffentlicher Name:&#039;&#039;&#039; zeigt auf die öffentliche IP-Adresse des Proxys.&lt;br /&gt;
* &#039;&#039;&#039;Zertifikat:&#039;&#039;&#039; Der Proxy braucht ein öffentlich gültiges Zertifikat für diesen Namen. Die automatische Zertifikatserneuerung des REST-Servers wird nicht benötigt.&lt;br /&gt;
* &#039;&#039;&#039;Weiterleitung:&#039;&#039;&#039; Der Proxy leitet per HTTPS an &amp;lt;code&amp;gt;https://&amp;lt;OBS-Server&amp;gt;:&amp;lt;Port&amp;gt;&amp;lt;/code&amp;gt; weiter. Für diese Strecke genügt am REST-Server ein Zertifikat Ihrer internen Zertifizierungsstelle oder ein selbst signiertes, dem der Proxy vertraut.&lt;br /&gt;
* &#039;&#039;&#039;Header:&#039;&#039;&#039; Alle HTTP-Methoden und Header unverändert durchreichen, insbesondere &#039;&#039;Authorization&#039;&#039;, &#039;&#039;apikey&#039;&#039; und &#039;&#039;Idempotency-Key&#039;&#039;. Antworten nicht zwischenspeichern. Den Header &#039;&#039;X-Forwarded-For&#039;&#039; mit der IP-Adresse des Konsumenten setzen.&lt;br /&gt;
* &#039;&#039;&#039;Anfragegrösse:&#039;&#039;&#039; Die maximale Anfragegrösse am Proxy darf nicht unter der Upload-Grenze der Endpunkte liegen.&lt;br /&gt;
* &#039;&#039;&#039;Firewall:&#039;&#039;&#039; Den Port des REST-Servers intern nur für den Proxy freigeben.&lt;br /&gt;
&lt;br /&gt;
Hinter dem Proxy verhält sich der REST-Server so:&lt;br /&gt;
&lt;br /&gt;
* Protokoll und Konsole zeigen als Host die IP-Adresse des Proxys und hängen den Wert von &#039;&#039;X-Forwarded-For&#039;&#039; als &amp;lt;code&amp;gt;[xff=…]&amp;lt;/code&amp;gt; an.&lt;br /&gt;
* Die [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge#Host-Beschränkung|Host-Beschränkung]] eines Zugangs kann keine einzelnen Konsumenten unterscheiden.&lt;br /&gt;
* Beim [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile#Rate-Limit je Profil|Rate-Limit]] teilen sich alle Konsumenten ohne API-Key einen Zähler.&lt;br /&gt;
&lt;br /&gt;
==Wartung der Server-Tabellen==&lt;br /&gt;
&lt;br /&gt;
Die Tabellen des REST-Servers pflegen sich selbst; ein regelmässiger Eingriff ist&lt;br /&gt;
nicht vorgesehen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Tabelle !! Aufräumen&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_IDEMPOTENCY || Abgeschlossene Einträge werden nach 30 Tagen gelöscht. Offene Reservierungen werden ab 24 Stunden im Protokoll gemeldet und &#039;&#039;&#039;anschliessend ebenfalls gelöscht&#039;&#039;&#039; - sie sind trotzdem ein Support-Fall, weil die Fachwirkung unbekannt ist, siehe Stolpersteine&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_TOKEN || Zeilen werden gelöscht, sobald Access- &#039;&#039;&#039;und&#039;&#039;&#039; Refresh-Laufzeit vorbei sind. Läuft beim Serverstart und danach höchstens stündlich&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_PROTO / RESTSRV_STATS || Wachsen mit dem Betrieb. Bei hohem Aufkommen empfiehlt sich ein turnusmässiges Archivieren&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Häufige Stolpersteine==&lt;br /&gt;
&lt;br /&gt;
* Bindung angelegt, aber der Server wurde nicht neugestartet -&amp;gt; die Bindung ist noch nicht aktiv.&lt;br /&gt;
* Endpunkt einem falschen Server zugeordnet -&amp;gt; Aufruf liefert 404 &#039;&#039;Endpunkt nicht vorhanden oder inaktiv&#039;&#039;.&lt;br /&gt;
* Pfad-Template falsch geschrieben oder Segmentzahl passt nicht -&amp;gt; kein Template matcht, Aufruf liefert 404.&lt;br /&gt;
* Zugang für den Endpunkt nicht freigeschaltet -&amp;gt; Aufruf liefert 403 &#039;&#039;Keine Berechtigung für die Endpunkt-Nutzung&#039;&#039;.&lt;br /&gt;
* Falscher API-Key oder falscher Host beim Zugang -&amp;gt; Aufruf liefert 401 bzw. 403.&lt;br /&gt;
* mTLS aktiv, aber kein Client-Zertifikat installiert -&amp;gt; Verbindung wird während des TLS-Handshakes abgebrochen.&lt;br /&gt;
* Datei-Upload ohne gesetzten Haken &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; am Endpunkt -&amp;gt; Aufruf liefert 415, das Skript läuft nicht an.&lt;br /&gt;
* Skript geändert, aber der Aufruf verhält sich noch wie vorher -&amp;gt; Endpunkt-Cache abwarten (bis zu 60 Sekunden).&lt;br /&gt;
* Schreibaufruf (POST/PUT/PATCH/DELETE) liefert 400 &#039;&#039;IDEMPOTENCY_KEY_MISSING&#039;&#039; -&amp;gt; der Client sendet keinen &#039;&#039;Idempotency-Key&#039;&#039;. Der Header ist seit v1.21 &#039;&#039;&#039;Pflicht&#039;&#039;&#039;, nicht optional; das Endpunkt-Skript läuft in diesem Fall gar nicht an. Betrifft typischerweise Konsumenten, die vor dem Update gebaut wurden.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 422 &#039;&#039;IDEMPOTENCY_KEY_REUSED&#039;&#039; -&amp;gt; der Client sendet denselben &#039;&#039;Idempotency-Key&#039;&#039; mit geändertem Inhalt. Pro Vorgang genau ein Schlüssel, über alle Wiederholversuche hinweg unverändert.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 409 &#039;&#039;IDEMPOTENCY_UNRESOLVED&#039;&#039; -&amp;gt; ein früherer Versuch wurde hart abgebrochen (Dienst beendet). Ob gebucht wurde, ist offen: im Protokoll und in den Fachdaten prüfen. Der Aufräumlauf meldet den Eintrag nach 24 Stunden im Protokoll und löscht ihn dann; wer schneller weitermachen will, entfernt ihn in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;.&lt;br /&gt;
* Nach einem Update antworten &#039;&#039;&#039;alle&#039;&#039;&#039; bestehenden Token mit 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; -&amp;gt; erwartetes Verhalten. Token, die vor der Einführung der Sitzungsprüfung ausgestellt wurden, gehören zu keiner Sitzung; die Konsumenten müssen sich einmalig neu anmelden.&lt;br /&gt;
* Anmeldung liefert 503 statt eines Tokens -&amp;gt; die Sitzungszeile konnte nicht geschrieben werden. Ursache im Protokoll unter &#039;&#039;TokenStore: ...&#039;&#039; nachsehen; typisch ist eine fehlende Tabelle &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; nach einem Update ohne Datenbank-Abgleich.&lt;br /&gt;
* Abmelden liefert 503 -&amp;gt; die Sitzung liess sich nicht sperren. Der Client ist &#039;&#039;&#039;nicht&#039;&#039;&#039; abgemeldet und muss den Aufruf wiederholen; Details im Protokoll.&lt;br /&gt;
* Techniker wird beim Arbeiten unerwartet abgemeldet -&amp;gt; im Protokoll nach &#039;&#039;WIEDERVORLAGE&#039;&#039; suchen. Ein doppelt verwendetes Refresh-Token sperrt die Sitzung. Legt die App Token mehrfach parallel ab, gehört das clientseitig behoben.&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=67057</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Server-Profile</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=67057"/>
		<updated>2026-10-26T08:22:18Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Zertifikatserneuerung und Rate-Limit: Hinweise zum Betrieb hinter einem Reverse-Proxy ergänzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Server-Profile=&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Server&#039;&#039;&#039; im REST-Modul ist ein eigenständiges TLS-Profil mit einer eigenen HTTP-Server-Instanz. In OBS können beliebig viele Server-Profile parallel betrieben werden, jedes mit eigenem Modus, eigenen Zertifikaten und eigenen Bindungen.&lt;br /&gt;
&lt;br /&gt;
Typische Einsatzfälle für mehrere Profile:&lt;br /&gt;
&lt;br /&gt;
* Ein &#039;&#039;&#039;Public-Profil&#039;&#039;&#039; mit Standard-TLS auf Port 443 für externe Konsumenten.&lt;br /&gt;
* Ein &#039;&#039;&#039;Internal-mTLS-Profil&#039;&#039;&#039; auf einem internen Port für Microservices, die zwingend ein Client-Zertifikat vorlegen müssen.&lt;br /&gt;
* Ein &#039;&#039;&#039;Debug-Profil&#039;&#039;&#039; mit Plain-HTTP auf 127.0.0.1, ausschliesslich für lokale Tests.&lt;br /&gt;
&lt;br /&gt;
==Aufruf==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Server&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Felder des Server-Profils==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Nr                  || Eindeutige Nummer 1-99, einmalig vergeben, kann später nicht mehr geändert werden&lt;br /&gt;
|-&lt;br /&gt;
| Name                || Sprechender Name, eindeutig - erscheint in Protokoll, Statistik und Endpunkt-Zuordnung&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv               || Profil wird beim Start des REST-Dienstes geladen. Inaktive Profile starten nicht&lt;br /&gt;
|-&lt;br /&gt;
| zusätzliche Infos   || Freitext zur Dokumentation (interner Kommentar, wird nicht ausgewertet)&lt;br /&gt;
|-&lt;br /&gt;
| kein SSL            || Plain-HTTP-Modus, ausschliesslich für Debug. TLS-Felder werden ignoriert&lt;br /&gt;
|-&lt;br /&gt;
| mTLS-Modus aktiv    || Mutual TLS: jeder Client muss ein gültiges Client-Zertifikat vorlegen&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikat (cert)   || PEM-Inhalt des Server-Zertifikats (inkl. ggf. Zwischen-CA-Kette)&lt;br /&gt;
|-&lt;br /&gt;
| Schlüssel (key)     || PEM-Inhalt des privaten Schlüssels&lt;br /&gt;
|-&lt;br /&gt;
| Root-Zertifikat (ca) || PEM-Inhalt des CA-Zertifikats, gegen das &#039;&lt;br /&gt;
|-&lt;br /&gt;
| Rate-Limit-Fenster  || Länge des Rate-Limit-Fensters in Sekunden. Leer/0 = Standard 60&lt;br /&gt;
|-&lt;br /&gt;
| Erfolgsanfragen     || Erlaubte erfolgreiche Anfragen je Schlüssel und Fenster. Leer/0 = Standard 120&lt;br /&gt;
|-&lt;br /&gt;
| Fehlanfragen        || Erlaubte fehlgeschlagene Anfragen je Schlüssel und Fenster. Leer/0 = Standard 10&lt;br /&gt;
|-&lt;br /&gt;
| Anfragen            || Erlaubte Anfragen des gesamten Profils je Fenster. Leer/0 = Standard 600&lt;br /&gt;
|-&lt;br /&gt;
| max. Verbindungen   || Obergrenze gleichzeitiger &#039;&#039;&#039;Verbindungen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_conn&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Sie deckt auch laufende Datei-Downloads ab, weil der Platz erst beim Trennen frei wird. Bei Überschreitung trennt der Server &#039;&#039;&#039;ohne HTTP-Antwort&#039;&#039;&#039; - der Wert gehört deutlich über den Normalbetrieb, als Reissleine&lt;br /&gt;
|-&lt;br /&gt;
| max. Anfragen       || Obergrenze gleichzeitig &#039;&#039;&#039;verarbeiteter Anfragen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_parallel&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Bei Überschreitung antwortet der Server &#039;&#039;&#039;503&#039;&#039;&#039; mit &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;, der Client darf es also gleich erneut versuchen&lt;br /&gt;
|-&lt;br /&gt;
| keine Kompression   || Antworten dieses Profils werden &#039;&#039;&#039;nicht&#039;&#039;&#039; gepackt (&amp;lt;code&amp;gt;rsv_no_gzip&amp;lt;/code&amp;gt;). Ohne Haken packt der Server JSON- und Textantworten ab 1024 Byte mit gzip, sofern der Client sie über &amp;lt;code&amp;gt;Accept-Encoding&amp;lt;/code&amp;gt; annimmt. Der Haken betrifft nur die Antwortrichtung - komprimierte &#039;&#039;&#039;Anfragen&#039;&#039;&#039; nimmt der Server weiterhin entgegen&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;max. Verbindungen&#039;&#039; und &#039;&#039;max. Anfragen&#039;&#039; sind die beiden&lt;br /&gt;
&#039;&#039;&#039;Lastgrenzen&#039;&#039;&#039; des Profils. Davon zu unterscheiden sind die festen Grenzen der&lt;br /&gt;
persistenten Verbindungen - 10 Sekunden Leerlauf, 100 Anfragen je Verbindung -,&lt;br /&gt;
die nicht der Last dienen, sondern der Hygiene und nicht einstellbar sind. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer|REST-Server]], Abschnitt Persistente&lt;br /&gt;
Verbindungen.}}&lt;br /&gt;
&lt;br /&gt;
Die Inhalte der Zertifikate werden direkt in OBS als Text gespeichert. Zur Laufzeit schreibt der Server die PEM-Inhalte für den Bruchteil einer Sekunde in ein Temp-Verzeichnis, damit OpenSSL sie beim Start lesen kann; danach werden die Temp-Dateien sofort wieder gelöscht.&lt;br /&gt;
&lt;br /&gt;
==Modi==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Modus               !! Voraussetzungen                       !! Anwendung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039;  || Zertifikat + Schlüssel                || Empfohlener Default für alle Server, die über das öffentliche Netz erreichbar sind&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;mTLS&#039;&#039;&#039;          || Zertifikat + Schlüssel + Root-Zert.   || Maschine-zu-Maschine-Kommunikation, in der jeder Client mit Cert validiert wird&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Plain-HTTP&#039;&#039;&#039;    || Haken &#039;&#039;kein SSL&#039;&#039;                    || Ausschliesslich lokale Tests. Erkennt TLS-Versuche auf dem Plain-Port und trennt sie direkt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beide Richtungen werden erkannt: Eine &#039;&#039;&#039;unverschlüsselte Anfrage an einen&lt;br /&gt;
TLS-Port&#039;&#039;&#039; (&#039;&#039;http&#039;&#039; statt &#039;&#039;https&#039;&#039;) beantwortet der Server mit &#039;&#039;&#039;HTTP 400&#039;&#039;&#039;&lt;br /&gt;
und einem Klartext-Hinweis. Die Abweisung erfolgt &#039;&#039;&#039;vor&#039;&#039;&#039; Authentifizierung,&lt;br /&gt;
Rate-Limit und Endpunkt-Skript - ein falsch konfigurierter Client löst also&lt;br /&gt;
keine Verarbeitung aus.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Bei Standard-TLS und mTLS fährt der Server ohne Zertifikat und Schlüssel gar nicht erst hoch. Bei aktiver Erneuerung trägt der Dienst beides selbst ein. Plain-HTTP umgeht TLS komplett und ist nicht für Produktivumgebungen geeignet.}}&lt;br /&gt;
&lt;br /&gt;
==TLS-Version, Cipher und Zertifikats-Rotation==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TLS-Version:&#039;&#039;&#039; Der Server erzwingt &#039;&#039;&#039;TLS 1.2&#039;&#039;&#039;. Die Cipher-Liste ist auf ECDHE-Schlüsselaustausch (Forward Secrecy) mit AEAD-Verfahren (AES-GCM, ChaCha20-Poly1305) beschränkt; CBC, statisches RSA-KEX und DHE sind ausgeschlossen.&lt;br /&gt;
* &#039;&#039;&#039;HSTS:&#039;&#039;&#039; Jede Antwort eines TLS-Profils trägt &#039;&#039;Strict-Transport-Security: max-age=31536000; includeSubDomains&#039;&#039;. Ein Browser spricht die Adresse damit ein Jahr lang nur noch über HTTPS an. Beim Profil &#039;&#039;Kein SSL&#039;&#039; wird der Header bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; gesetzt - der Browser würde ihn sonst speichern und die Erzwingung auch auf produktive Sitzungen anwenden.&lt;br /&gt;
* &#039;&#039;&#039;Rotation:&#039;&#039;&#039; Bei &#039;&#039;&#039;automatischer Erneuerung&#039;&#039;&#039; tauscht der Dienst das Zertifikat selbst und startet dabei nur das betroffene Server-Profil neu; die übrigen Profile und der Dienst laufen weiter. Bei manueller Pflege ändern Sie die PEM-Inhalte und starten den REST-Dienst neu - bis dahin läuft das alte Zertifikat weiter.&lt;br /&gt;
* &#039;&#039;&#039;Certificate-Pinning:&#039;&#039;&#039; Mobile Clients können den Server-Public-Key pinnen. Zertifikatswechsel daher &#039;&#039;&#039;rechtzeitig ankündigen&#039;&#039;&#039; und mit einem &#039;&#039;&#039;Backup-Pin&#039;&#039;&#039; vorbereiten - der neue Pin muss vor dem Wechsel in einer Client-Version ausgerollt sein, sonst sperrt sich die App aus.&lt;br /&gt;
==Automatische Zertifikatserneuerung==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server kann sein TLS-Zertifikat selbst bei einer Zertifizierungsstelle beantragen und vor Ablauf erneuern. Zertifikat und privater Schlüssel entstehen dabei auf Ihrem Server und verlassen ihn nicht.&lt;br /&gt;
&lt;br /&gt;
===Voraussetzungen===&lt;br /&gt;
&lt;br /&gt;
* Ein öffentlicher Name, der von aussen auf diesen Server zeigt.&lt;br /&gt;
* &#039;&#039;&#039;Port 80&#039;&#039;&#039; ist von aussen auf diesen Server weitergeleitet. Darüber prüft die Zertifizierungsstelle, dass Sie den Namen betreiben. Der Dienst öffnet den Port nur für die Dauer der Prüfung und schliesst ihn danach wieder.&lt;br /&gt;
* Ausgehende HTTPS-Verbindungen zur Zertifizierungsstelle sind erlaubt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Port 80 wird unabhängig davon benötigt, auf welchem Port der REST-Server selbst lauscht. Die Prüfung der Zertifizierungsstelle beginnt immer auf Port 80.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Nimmt ein Reverse-Proxy die TLS-Verbindung von aussen an, braucht nur der Proxy das öffentliche Zertifikat. Die automatische Zertifikatserneuerung und die Weiterleitung von Port 80 auf den REST-Server entfallen dann. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung#Betrieb hinter einem Reverse-Proxy|Betrieb hinter einem Reverse-Proxy]].}}&lt;br /&gt;
&lt;br /&gt;
===Felder===&lt;br /&gt;
&lt;br /&gt;
Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                       !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Automatische Erneuerung    || &#039;&#039;deaktiviert&#039;&#039; = aus. &#039;&#039;Produktivbetrieb&#039;&#039; = der Dienst beantragt und erneuert das Zertifikat selbst. &#039;&#039;Testbetrieb&#039;&#039; = siehe Warnung unten&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikatsname (FQDN)     || Der Name, auf den das Zertifikat ausgestellt wird. Pflicht, sobald die Erneuerung aktiv ist&lt;br /&gt;
|-&lt;br /&gt;
| Kontakt bei der CA         || Optional. Die Zertifizierungsstelle schickt an diese Adresse Warnungen, wenn ein Zertifikat abzulaufen droht&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikat gültig bis      || Ablaufdatum des aktiven Zertifikats&lt;br /&gt;
|-&lt;br /&gt;
| Testzertifikat aus Staging || Gesetzt, wenn das aktive Zertifikat aus dem Testbetrieb stammt&lt;br /&gt;
|-&lt;br /&gt;
| Zustand                    || &#039;&#039;idle&#039;&#039; = nichts zu tun, &#039;&#039;pending&#039;&#039; = Vorgang läuft, &#039;&#039;error&#039;&#039; = letzter Versuch fehlgeschlagen&lt;br /&gt;
|-&lt;br /&gt;
| Letzte Meldung             || Ergebnis des letzten Vorgangs - im Störungsfall das erste Feld, in das Sie sehen&lt;br /&gt;
|-&lt;br /&gt;
| Gesperrt bis               || Nach einem Fehlschlag wartet der Dienst bis zu diesem Zeitpunkt, bevor er es erneut versucht&lt;br /&gt;
|-&lt;br /&gt;
| Letzter Selbsttest am      || Zeitpunkt der letzten Erreichbarkeitsprüfung&lt;br /&gt;
|-&lt;br /&gt;
| Selbsttest bestanden       || Ergebnis der letzten Erreichbarkeitsprüfung&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die Felder ab &#039;&#039;Zertifikat gültig bis&#039;&#039; füllt der Dienst selbst; sie dienen der Anzeige.&lt;br /&gt;
&lt;br /&gt;
===Ablauf===&lt;br /&gt;
&lt;br /&gt;
Der Dienst sieht stündlich nach, ob etwas ansteht:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Selbsttest:&#039;&#039;&#039; Einmal wöchentlich prüft der Dienst über das Testsystem der Zertifizierungsstelle, ob Ihr Server von aussen erreichbar ist. Besteht der Test nicht, beantragt der Dienst kein Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Erneuerung:&#039;&#039;&#039; Ab 30 Tagen Restlaufzeit beantragt der Dienst ein neues Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Übernahme:&#039;&#039;&#039; Der Dienst prüft das neue Zertifikat, speichert es und bewahrt das bisherige als Rückfall auf. Anschliessend startet &#039;&#039;&#039;nur das betroffene Server-Profil&#039;&#039;&#039; neu.&lt;br /&gt;
# &#039;&#039;&#039;Rückfall:&#039;&#039;&#039; Startet das Profil mit dem neuen Zertifikat nicht, aktiviert der Dienst wieder das vorherige.&lt;br /&gt;
&lt;br /&gt;
Nach einem Fehlschlag wartet der Dienst zunehmend länger, bevor er es erneut versucht - eine, zwei, vier, acht und höchstens 24 Stunden. Das verhindert, dass ein dauerhafter Fehler das Anfragekontingent der Zertifizierungsstelle aufbraucht.&lt;br /&gt;
&lt;br /&gt;
===Befehle in der Konsole===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; || Zeigt je Profil Betriebsart, Ablaufdatum, Zustand und das Ergebnis des letzten Selbsttests&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; || Prüft sofort, ob der Server von aussen erreichbar ist. Beantragt kein Zertifikat&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; || Stösst die Erneuerung sofort an, auch wenn sie noch nicht fällig ist&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Testbetrieb===&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Im &#039;&#039;&#039;Testbetrieb&#039;&#039;&#039; beantragt der Dienst das Zertifikat beim Testsystem der Zertifizierungsstelle. Einem solchen Zertifikat vertraut &#039;&#039;&#039;kein&#039;&#039;&#039; Client - Anwendungen können sich danach nicht mehr mit diesem Server verbinden. Verwenden Sie den Testbetrieb ausschliesslich auf Testsystemen. Zurück in den Produktivbetrieb kommen Sie, indem Sie das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039; stellen; der Dienst holt daraufhin sofort ein gültiges Zertifikat, auch wenn das Testzertifikat noch lange läuft.}}&lt;br /&gt;
&lt;br /&gt;
==Rate-Limit je Profil==&lt;br /&gt;
&lt;br /&gt;
Jedes Server-Profil führt &#039;&#039;&#039;eigene Zähler&#039;&#039;&#039; und kann &#039;&#039;&#039;eigene Schwellen&#039;&#039;&#039; haben.&lt;br /&gt;
Ein Profil, das gerade überrannt wird, bremst damit die anderen Profile nicht mit&lt;br /&gt;
aus - das Public-Profil und das interne mTLS-Profil sind voneinander unabhängig.&lt;br /&gt;
&lt;br /&gt;
Gezählt wird pro Schlüssel, und der Schlüssel ist der &#039;&#039;&#039;API-Key&#039;&#039;&#039; des Zugangs.&lt;br /&gt;
Sendet ein Konsument keinen API-Key (Zugang mit &#039;&#039;*&#039;&#039;), wird ersatzweise auf die&lt;br /&gt;
&#039;&#039;&#039;IP-Adresse&#039;&#039;&#039; gezählt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Das ist der wichtigste Punkt bei mobilen Anwendungen: Sitzen alle&lt;br /&gt;
Benutzer hinter einer gemeinsamen Firmen-IP und sendet die App keinen eigenen&lt;br /&gt;
API-Key, teilen sich &#039;&#039;&#039;alle Benutzer einen Zähler&#039;&#039;&#039;. Mit den Standardwerten sind&lt;br /&gt;
das 10 Fehlversuche für den ganzen Betrieb pro Minute - nach ein paar falschen&lt;br /&gt;
Passworteingaben steht die Abteilung. Für solche Profile die Schwellen bewusst&lt;br /&gt;
höher setzen oder jedem Konsumenten einen eigenen API-Key geben.}}&lt;br /&gt;
&lt;br /&gt;
Dasselbe gilt hinter einem Reverse-Proxy: Alle Anfragen kommen dort von der IP-Adresse&lt;br /&gt;
des Proxys, und alle Konsumenten ohne API-Key teilen sich einen Zähler. Der Header&lt;br /&gt;
&#039;&#039;X-Forwarded-For&#039;&#039; ändert daran nichts, er wird nur protokolliert.&lt;br /&gt;
&lt;br /&gt;
Bleiben alle vier Felder leer (bzw. 0), gelten die Standardwerte. Die Werte werden&lt;br /&gt;
&#039;&#039;&#039;beim Start&#039;&#039;&#039; des REST-Dienstes gelesen - eine Änderung wird erst nach einem&lt;br /&gt;
Neustart wirksam.&lt;br /&gt;
&lt;br /&gt;
==Validierung beim Speichern==&lt;br /&gt;
&lt;br /&gt;
Immer geprüft wird:&lt;br /&gt;
&lt;br /&gt;
* Nr und Name sind vergeben, Nr liegt zwischen 1 und 99.&lt;br /&gt;
* Nr und Name sind eindeutig.&lt;br /&gt;
* Eingetragene Zertifikate und Schlüssel liegen im PEM-Format vor.&lt;br /&gt;
* Ein eingetragener Zertifikatsname enthält keine Adresse, keinen Port und keine Leerzeichen.&lt;br /&gt;
&lt;br /&gt;
Nur bei &#039;&#039;&#039;aktivem&#039;&#039;&#039; Profil zusätzlich:&lt;br /&gt;
&lt;br /&gt;
* Ohne &#039;&#039;kein SSL&#039;&#039; und ohne automatische Erneuerung sind Zertifikat und Schlüssel vorhanden.&lt;br /&gt;
* Bei &#039;&#039;mTLS-Modus aktiv&#039;&#039; ist das Root-Zertifikat vorhanden.&lt;br /&gt;
* Bei aktiver Erneuerung ist der Zertifikatsname vergeben, und &#039;&#039;kein SSL&#039;&#039; ist nicht gesetzt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein &#039;&#039;&#039;nicht aktives&#039;&#039;&#039; Profil dürfen Sie unvollständig speichern - es wird beim Start des Dienstes nicht geladen. Was Sie eintragen, wird trotzdem auf Plausibilität geprüft.}}&lt;br /&gt;
&lt;br /&gt;
==Bindungen==&lt;br /&gt;
&lt;br /&gt;
Eine Bindung ist die Kombination aus IP-Adresse und Port, auf der ein Server-Profil lauscht. Pro Server-Profil können mehrere Bindungen existieren, z.B. um die gleiche TLS-Konfiguration parallel auf 443 und 8443 anzubieten.&lt;br /&gt;
&lt;br /&gt;
===Aufruf===&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste den gewünschten Server markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die zum Server gehörende Bindungs-Liste.&lt;br /&gt;
&lt;br /&gt;
===Felder===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld    !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Host    || IP-Adresse oder Hostname, auf dem gelauscht wird. &#039;&#039;0.0.0.0&#039;&#039; = alle IPv4-Adressen; &#039;&#039;127.0.0.1&#039;&#039; = nur lokal; explizite IPs binden gezielt auf eine Netzwerkkarte&lt;br /&gt;
|-&lt;br /&gt;
| Port    || TCP-Port (1-65535). 443/8443 für HTTPS, frei wählbar für Debug-Profile&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv   || Bindung wird beim Server-Start geladen&lt;br /&gt;
|-&lt;br /&gt;
| Standard|| Markiert die Standard-Bindung des Servers. Pro Server-Profil ist nur eine Standard-Bindung erlaubt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Wirksamkeit===&lt;br /&gt;
&lt;br /&gt;
Bindungen werden ausschliesslich beim Start des REST-Dienstes geladen. Änderungen werden daher erst nach einem Neustart aktiv. Bis dahin laufen die alten Bindungen weiter.&lt;br /&gt;
&lt;br /&gt;
==Listen-Funktionen==&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfg&#039;&#039;&#039; - neues Server-Profil&lt;br /&gt;
* &#039;&#039;&#039;Return&#039;&#039;&#039; - vorhandenes Profil bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F4&#039;&#039;&#039; - Sortierung wählen&lt;br /&gt;
* &#039;&#039;&#039;F6&#039;&#039;&#039; - Bindungs-Liste des markierten Servers&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zugaenge&amp;diff=67056</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Zugaenge</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zugaenge&amp;diff=67056"/>
		<updated>2026-10-26T08:22:18Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Host-Beschränkung und RESTSRV_TOKEN: Verhalten hinter einem Reverse-Proxy ergänzt (rt_ip_fwd) [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Zugänge=&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Zugang&#039;&#039;&#039; (Account) ist der Kanal, über den ein externer Konsument den REST-Server anspricht. Er bündelt API-Key, Host-Beschränkung, CORS-Konfiguration, optionale JWT-Authentifizierung und optionale mTLS-Subject-Prüfung.&lt;br /&gt;
&lt;br /&gt;
==Aufruf==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Zugänge&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Felder==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld          !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Name          || Sprechender Name, erscheint im Protokoll und der Statistik&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv         || Inaktive Zugänge werden bei der Authentifizierung mit 403 abgewiesen&lt;br /&gt;
|-&lt;br /&gt;
| API-Key       || Eindeutiger Schlüssel, den der Konsument im HTTP-Header &#039;&#039;apikey&#039;&#039; senden muss. Per Schlüsselsymbol generierbar&lt;br /&gt;
|-&lt;br /&gt;
| Host          || Optionaler Hostname oder IP-Adresse des Konsumenten. Stimmt die anfragende IP nicht überein, wird mit 403 abgelehnt&lt;br /&gt;
|-&lt;br /&gt;
| CORS-Origins  || Liste erlaubter Origins für Browser-Aufrufe, mehrere durch &#039;&#039;;&#039;&#039; getrennt. &#039;&#039;&#039;Leer heisst nicht „keine Einschränkung&amp;quot;, sondern „kein Browser-Zugriff&amp;quot;&#039;&#039;&#039; - siehe unten&lt;br /&gt;
|-&lt;br /&gt;
| mTLS-Subject  || Erwartete RDN-Komponente(n) im Client-Zertifikat-Subject, Komma-/Semikolon-getrennt (alle müssen vorkommen; nur sinnvoll bei mTLS-Servern)&lt;br /&gt;
|-&lt;br /&gt;
| JWT           || Schalter, ob für diesen Zugang ein JWT-Token nötig ist&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Endpunkt  || Pfad, über den &#039;&#039;&#039;alle Token-Vorgänge&#039;&#039;&#039; laufen: Anmelden, Erneuern und Abmelden (z.B. &#039;&#039;oauth&#039;&#039; oder &#039;&#039;meineapp/dev/auth&#039;&#039;). Verglichen wird der &#039;&#039;&#039;vollständige Pfad&#039;&#039;&#039;, mehrstufige Angaben mit &#039;&#039;/&#039;&#039; sind also erlaubt&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Key       || Geheimer Schlüssel zur Signierung/Verifikation, per Schlüsselsymbol generierbar&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Exp       || Gültigkeitsdauer eines ausgestellten Access-Tokens in &#039;&#039;&#039;Minuten&#039;&#039;&#039;. Derselbe Wert wird dem Konsumenten in der Token-Antwort als &#039;&#039;expiresIn&#039;&#039; in &#039;&#039;&#039;Sekunden&#039;&#039;&#039; mitgeteilt und steht dem Endpunkt-Skript als &#039;&#039;_OBS_JWT_EXP_SEC&#039;&#039; zur Verfügung&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==API-Key==&lt;br /&gt;
&lt;br /&gt;
Der API-Key ist die Basis-Authentifizierung. Der Client übergibt ihn im HTTP-Header:&lt;br /&gt;
&lt;br /&gt;
 apikey: abcd-efgh-ijkl-mnop-qrstuvwxyz12&lt;br /&gt;
&lt;br /&gt;
Wird ein Aufruf ohne &#039;&#039;apikey&#039;&#039;-Header gestellt, sucht der Server intern nach einem Zugang mit API-Key &#039;&#039;&#039;*&#039;&#039;&#039; - damit lassen sich (mit Vorsicht!) auch öffentliche Endpunkte ohne Authentifizierung realisieren.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein Zugang mit API-Key &#039;&#039;*&#039;&#039; sollte nur für bewusst freigegebene Endpunkte verwendet werden und immer in Kombination mit einer Host-Beschränkung oder CORS-Beschränkung betrieben werden.}}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Nur für wirklich öffentliche Dateien.&#039;&#039;&#039; Ohne API-Key kennt der&lt;br /&gt;
Endpunkt weder Mandant noch Rolle - es gibt nichts, woran er eine Berechtigung&lt;br /&gt;
festmachen könnte ausser dem, was in der URL steht. Ein Wildcard-Zugang eignet&lt;br /&gt;
sich deshalb für Artefakte, die für jeden bestimmt sind (Test-APK, Handbuch),&lt;br /&gt;
&#039;&#039;&#039;nicht&#039;&#039;&#039; für Kundenbelege. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]].}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Bei einem Wildcard-Zugang sollte &#039;&#039;CORS-Origin&#039;&#039; &#039;&#039;&#039;leer&#039;&#039;&#039; bleiben.&lt;br /&gt;
Ein &#039;&#039;*&#039;&#039; dort wirkt auf die Preflight-Antwort &#039;&#039;&#039;aller&#039;&#039;&#039; Zugänge (siehe unten);&lt;br /&gt;
ein Download über die Adresszeile des Browsers sendet ohnehin kein &#039;&#039;Origin&#039;&#039;,&lt;br /&gt;
CORS greift dort also gar nicht.}}&lt;br /&gt;
&lt;br /&gt;
==Host-Beschränkung==&lt;br /&gt;
&lt;br /&gt;
Ist im Feld &#039;&#039;&#039;Host&#039;&#039;&#039; ein Wert hinterlegt, akzeptiert der Server Anfragen unter diesem Zugang nur dann, wenn die Remote-IP exakt diesem Host entspricht oder durch DNS-Auflösung auf diese IP zeigt. DNS-Auflösungen werden 5 Minuten gecached.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Hinter einem Reverse-Proxy ist die Remote-IP immer die des Proxys. Die Host-Beschränkung kann dort keine einzelnen Konsumenten unterscheiden. Der Header &#039;&#039;X-Forwarded-For&#039;&#039; wird dafür bewusst nicht ausgewertet, weil jeder Client ihn selbst setzen kann.}}&lt;br /&gt;
&lt;br /&gt;
==CORS==&lt;br /&gt;
&lt;br /&gt;
Soll ein Endpunkt aus einer Browser-Anwendung (Single-Page-App, JavaScript) heraus aufgerufen werden, muss das Origin der Anwendung in der CORS-Liste des Zugangs eingetragen sein.&lt;br /&gt;
&lt;br /&gt;
Format der Liste:&lt;br /&gt;
&lt;br /&gt;
 https://app.kunde.de;https://test.kunde.de;https://localhost:8080&lt;br /&gt;
&lt;br /&gt;
Ablauf:&lt;br /&gt;
&lt;br /&gt;
# Browser sendet einen Preflight-Request (&#039;&#039;OPTIONS&#039;&#039;). Der REST-Server prüft das Origin gegen die globale Origin-Liste &#039;&#039;&#039;aller&#039;&#039;&#039; Accounts (Cache, TTL 60s) und antwortet mit 204 + CORS-Header, falls erlaubt.&lt;br /&gt;
# Der eigentliche Request wird gegen die Origin-Liste &#039;&#039;&#039;des aktuell verwendeten Zugangs&#039;&#039;&#039; geprüft.&lt;br /&gt;
# Stimmt das Origin nicht, wird mit 403 &#039;&#039;Origin nicht erlaubt für Account ...&#039;&#039; abgelehnt.&lt;br /&gt;
&lt;br /&gt;
Der Preflight kann nicht zugangsbezogen prüfen: der Browser schickt dabei keinen&lt;br /&gt;
&#039;&#039;apikey&#039;&#039; mit, der Zugang ist zu diesem Zeitpunkt unbekannt. &#039;&#039;&#039;Die Durchsetzung&lt;br /&gt;
liegt deshalb auf Schritt 2&#039;&#039;&#039;, nicht auf dem Preflight. Praktische Folge: trägt&lt;br /&gt;
&#039;&#039;&#039;ein&#039;&#039;&#039; Zugang auf dem Server ein &#039;&#039;*&#039;&#039; in seiner Liste, antwortet jeder&lt;br /&gt;
Preflight mit &#039;&#039;Access-Control-Allow-Origin: *&#039;&#039; - auch für Zugänge mit engerer&lt;br /&gt;
Liste. Deren eigentliche Anfrage wird trotzdem gegen die eigene Liste geprüft.&lt;br /&gt;
&lt;br /&gt;
Erlaubte Methoden: &#039;&#039;GET, POST, PUT, DELETE, PATCH&#039;&#039; (systemseitig festgelegt).&lt;br /&gt;
&lt;br /&gt;
Bei den &#039;&#039;&#039;Anfrage-Headern&#039;&#039;&#039; spiegelt der Server im Preflight zurück, was der&lt;br /&gt;
Browser in &#039;&#039;Access-Control-Request-Headers&#039;&#039; angefragt hat. Damit sind eigene&lt;br /&gt;
Header eines Projekts - &#039;&#039;Idempotency-Key&#039;&#039;, &#039;&#039;If-Match&#039;&#039;, später&lt;br /&gt;
&#039;&#039;Upload-Offset&#039;&#039;/&#039;&#039;Upload-Length&#039;&#039; - ohne Änderung am Server nutzbar. Fragt der&lt;br /&gt;
Preflight keine Header an, antwortet der Server mit der Liste&lt;br /&gt;
&#039;&#039;Content-Type, apikey, Authorization, Accept&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Die Header-Liste ist &#039;&#039;&#039;keine Schutzgrenze&#039;&#039;&#039; - das ist das Origin. Sie&lt;br /&gt;
sagt dem Browser nur, welche Header der Server versteht. Dazu kommt, dass die&lt;br /&gt;
Zugangsdaten dieser Schnittstelle in Headern stehen (&#039;&#039;apikey&#039;&#039;,&lt;br /&gt;
&#039;&#039;Authorization&#039;&#039;) und nicht in Cookies: es gibt also keine automatisch&lt;br /&gt;
mitgeschickte Berechtigung, die eine fremde Seite ausnutzen könnte.}}&lt;br /&gt;
&lt;br /&gt;
===Was „leer&amp;quot; bedeutet===&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Ein &#039;&#039;&#039;leeres&#039;&#039;&#039; Feld CORS-Origins heisst &#039;&#039;&#039;nicht&#039;&#039;&#039; „keine&lt;br /&gt;
Einschränkung&amp;quot;. Es heisst: &#039;&#039;&#039;jeder Aufruf mit &#039;&#039;Origin&#039;&#039;-Header wird mit 403&lt;br /&gt;
abgelehnt&#039;&#039;&#039;. Für Clients &#039;&#039;&#039;ohne&#039;&#039;&#039; &#039;&#039;Origin&#039;&#039;-Header - mobile Apps, Server,&lt;br /&gt;
curl - ändert das Feld gar nichts, sie werden nie gegen CORS geprüft.}}&lt;br /&gt;
&lt;br /&gt;
Daraus ergeben sich drei sinnvolle Einstellungen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| leer || Kein Browser-Zugriff. Richtig für Zugänge, die nur von Apps oder Servern benutzt werden&lt;br /&gt;
|-&lt;br /&gt;
| konkrete Origins || Browser-Zugriff nur von diesen Seiten. Der Regelfall für Web-Anwendungen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;*&#039;&#039; || Browser-Zugriff von überall. Wirkt zusätzlich auf die Preflight-Antwort aller anderen Zugänge - bewusst und sparsam verwenden&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Für eine Browser-Anwendung ist dieses Feld besonders wichtig, weil ihr API-Key&lt;br /&gt;
zwangsläufig im JavaScript liegt und damit öffentlich ist. Die Origin-Liste&lt;br /&gt;
verhindert dann, dass jemand den Key nimmt und von seiner eigenen Seite benutzt.&lt;br /&gt;
Sie ist allerdings kein Ersatz für eine Zugriffskontrolle: wer den Key hat,&lt;br /&gt;
umgeht CORS mit einem einzigen Aufruf ausserhalb des Browsers.&lt;br /&gt;
&lt;br /&gt;
==JWT-Authentifizierung==&lt;br /&gt;
&lt;br /&gt;
Wird ein Zugang mit aktiver JWT-Pflicht aufgerufen, sind die folgenden Phasen zu durchlaufen (Phase 3 nur bei genutztem Refresh).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Anmelden, Erneuern und Abmelden laufen über denselben Endpunkt&#039;&#039;&#039; - den im&lt;br /&gt;
Feld JWT-Endpunkt hinterlegten Pfad. Ein eigener Endpunkt fürs Abmelden ist&lt;br /&gt;
nicht nötig und wäre auch umständlicher: der JWT-Endpunkt greift &#039;&#039;&#039;vor&#039;&#039;&#039; der&lt;br /&gt;
Adressauflösung und braucht deshalb weder eine Zeile in &#039;&#039;RESTSRV_ENDPOINTS&#039;&#039;&lt;br /&gt;
noch eine Berechtigung in &#039;&#039;RESTSRV_ACCESS&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Unterschieden wird nach Verb und Token-Art:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Ablauf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; ohne &#039;&#039;Authorization&#039;&#039; || Anmelden (Skript-Methode &#039;&#039;Authenticate&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; mit Bearer, Refresh-Token || Erneuern (Skript-Methode &#039;&#039;Refresh&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;DELETE&#039;&#039; mit Bearer, Access-Token || Abmelden (kein Skript nötig)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; mit Bearer, Access-Token || &#039;&#039;&#039;401&#039;&#039;&#039; - unverändert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Dass das Abmelden am Verb hängt und nicht allein an der Token-Art, ist&lt;br /&gt;
Absicht. Würde ein vorgelegtes Access-Token beim &#039;&#039;POST&#039;&#039; als Abmeldung gedeutet,&lt;br /&gt;
würde ein Client, der seine beiden Token verwechselt, den Anwender still&lt;br /&gt;
abmelden - statt das klare 401 zu bekommen, an dem der Fehler auffällt.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 1: Token-Bezug===&lt;br /&gt;
&lt;br /&gt;
Der Konsument ruft den JWT-Endpunkt des Zugangs auf, z.B.:&lt;br /&gt;
&lt;br /&gt;
 POST https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
 Body:   {&amp;quot;username&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;password&amp;quot;:&amp;quot;...&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Der Server führt das im Zugang hinterlegte &#039;&#039;&#039;Authentifizierungs-Skript&#039;&#039;&#039; aus (F7 in der Zugangs-Liste). Das Skript muss eine Prozedur &#039;&#039;Authenticate&#039;&#039; bereitstellen, die über den Writer schreibt:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;status&#039;&#039; = &#039;&#039;1&#039;&#039; bei Erfolg, &#039;&#039;9&#039;&#039; bei Misserfolg&lt;br /&gt;
* &#039;&#039;error&#039;&#039; = Fehlertext (bei status=9)&lt;br /&gt;
* &#039;&#039;_OBS_JWT_ID&#039;&#039;, &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039;, &#039;&#039;_OBS_JWT_AUDIENCE&#039;&#039; für die JWT-Claims (optional)&lt;br /&gt;
* &#039;&#039;_OBS_JWT_CLAIM_&amp;amp;lt;name&amp;amp;gt;&#039;&#039; für beliebige Custom-Claims (z.B. Mandant &#039;&#039;tenant&#039;&#039;, Rollen &#039;&#039;roles&#039;&#039;); im Folge-Skript lesbar&lt;br /&gt;
* &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039; (optional) löst die Ausstellung eines Refresh-Tokens aus; &#039;&#039;_OBS_JWT_REFRESH_EXP&#039;&#039; setzt dessen Lebensdauer in Minuten (Default 90 Tage)&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Die Lebensdauer eines Refresh-Tokens ist nach oben auf &#039;&#039;&#039;372 Tage&#039;&#039;&#039;&lt;br /&gt;
begrenzt. Ein mit &#039;&#039;_OBS_JWT_REFRESH_EXP&#039;&#039; länger ausgestelltes Token wird bei der&lt;br /&gt;
Vorlage &#039;&#039;&#039;nicht mehr verifiziert&#039;&#039;&#039; und mit 401 abgewiesen - der Fehler fällt&lt;br /&gt;
also erst Monate später auf, beim ersten Erneuerungsversuch.}}&lt;br /&gt;
&lt;br /&gt;
Die Signatur lautet &#039;&#039;procedure Authenticate(oReader: TxRestReader; oWriter: TxRestWriter);&#039;&#039; - Details unter [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]]. Bei Erfolg signiert der Server einen Token (HS256, Issuer &#039;&#039;OBS REST-Server&#039;&#039;, Lebenszeit gemäß JWT-Exp) und antwortet:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;token&amp;quot;:        &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;accessToken&amp;quot;:  &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;:    28800,&lt;br /&gt;
  &amp;quot;serverTime&amp;quot;:   &amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Zum Aufbau der Antwort:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;token&#039;&#039; und &#039;&#039;accessToken&#039;&#039; enthalten &#039;&#039;&#039;denselben&#039;&#039;&#039; Token. &#039;&#039;accessToken&#039;&#039; ist der Name, den die meisten Client-Bibliotheken erwarten; &#039;&#039;token&#039;&#039; bleibt für bestehende Konsumenten erhalten.&lt;br /&gt;
* &#039;&#039;expiresIn&#039;&#039; ist die Gültigkeit in &#039;&#039;&#039;Sekunden&#039;&#039;&#039; (JWT-Exp × 60).&lt;br /&gt;
* &#039;&#039;&#039;Alle weiteren Felder, die das Authenticate-Skript zurückgibt, werden mit ausgeliefert&#039;&#039;&#039; - mit Ausnahme von &#039;&#039;status&#039;&#039; und den &#039;&#039;_OBS_&#039;&#039;-Steuerfeldern. So kann das Skript z.B. eine Benutzer-Id, den Mandanten oder Rollen direkt in die Anmelde-Antwort legen, ohne dass der Client dafür einen zweiten Aufruf braucht.&lt;br /&gt;
&lt;br /&gt;
Beispiel mit Zusatzfeldern aus dem Skript:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;benutzerId&amp;quot;: &amp;quot;4711&amp;quot;, &amp;quot;tenant&amp;quot;: &amp;quot;nord&amp;quot;, &amp;quot;roles&amp;quot;: [&amp;quot;TECHNIKER&amp;quot;],&lt;br /&gt;
  &amp;quot;token&amp;quot;: &amp;quot;eyJ...&amp;quot;, &amp;quot;accessToken&amp;quot;: &amp;quot;eyJ...&amp;quot;, &amp;quot;refreshToken&amp;quot;: &amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;: 28800, &amp;quot;serverTime&amp;quot;: &amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Mit der Ausstellung legt der Server zu dem Token-Paar eine &#039;&#039;&#039;Sitzungszeile&#039;&#039;&#039; in&lt;br /&gt;
&#039;&#039;RESTSRV_TOKEN&#039;&#039; an. Lässt sich diese Zeile nicht schreiben (Tabelle fehlt,&lt;br /&gt;
Datenbankproblem), wird &#039;&#039;&#039;kein Token ausgeliefert&#039;&#039;&#039;: der Server antwortet mit&lt;br /&gt;
&#039;&#039;&#039;503&#039;&#039;&#039; und &#039;&#039;Retry-After&#039;&#039;. Das ist bewusst von der abgelehnten Anmeldung&lt;br /&gt;
unterschieden - die Zugangsdaten waren in Ordnung, der Versuch darf wiederholt&lt;br /&gt;
werden.&lt;br /&gt;
&lt;br /&gt;
Wird die Anmeldung abgelehnt (&#039;&#039;status&#039;&#039; = 9), antwortet der Server mit&lt;br /&gt;
&#039;&#039;&#039;401 Unauthorized&#039;&#039;&#039;, dem Code &#039;&#039;AUTH_FAILED&#039;&#039; und reicht den Text aus dem Feld&lt;br /&gt;
&#039;&#039;error&#039;&#039; als &#039;&#039;error.message&#039;&#039; an den Client durch - der Konsument kann ihn also&lt;br /&gt;
direkt anzeigen. Die Formulierung liegt damit beim Skript.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;AUTH_FAILED&#039;&#039; unterscheidet die &#039;&#039;&#039;abgelehnte Anmeldung&#039;&#039;&#039; vom&lt;br /&gt;
&#039;&#039;AUTH_EXPIRED&#039;&#039;, das ein abgelaufenes oder gesperrtes Token liefert. Ein Client&lt;br /&gt;
sollte auf den Code reagieren und nicht auf den Text: Bei &#039;&#039;AUTH_FAILED&#039;&#039; gehört&lt;br /&gt;
die Anmeldemaske noch einmal vorgelegt, bei &#039;&#039;AUTH_EXPIRED&#039;&#039; erst ein Refresh&lt;br /&gt;
versucht.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der durchgereichte Text ist für den Endanwender gedacht und sollte&lt;br /&gt;
keine internen Details verraten. „Benutzer oder Passwort ist falsch&amp;quot; ist eine gute&lt;br /&gt;
Meldung, „Kein Datensatz in BENUTZ mit b_name=meier&amp;quot; nicht.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 2: Token-Verwendung===&lt;br /&gt;
&lt;br /&gt;
Bei allen folgenden Aufrufen sendet der Client den Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 GET https://api.meinserver.de/kalender/v1&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi...&lt;br /&gt;
&lt;br /&gt;
Der Server verifiziert Signatur, Issuer, Ausstellzeitpunkt und Ablauf (Toleranz: 20 Sekunden Clock-Skew) und prüft anschliessend die &#039;&#039;&#039;Sitzung&#039;&#039;&#039; in &#039;&#039;RESTSRV_TOKEN&#039;&#039;. Im Skript stehen die Claims als Parameter &#039;&#039;_OBS_JWT_ID&#039;&#039;, &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039; und &#039;&#039;_OBS_JWT_AUDIENCE&#039;&#039; zur Verfügung.&lt;br /&gt;
&lt;br /&gt;
Die Sitzungsprüfung weist ein Token ab, das zu keiner Zeile gehört oder dessen&lt;br /&gt;
Sitzung gesperrt wurde - beides mit &#039;&#039;&#039;401&#039;&#039;&#039; und dem Code &#039;&#039;AUTH_EXPIRED&#039;&#039;. Der&lt;br /&gt;
Grund steht jeweils im Protokoll &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; unter &#039;&#039;TokenStore: ...&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein Token, das &#039;&#039;&#039;vor&#039;&#039;&#039; der Einführung dieser Prüfung ausgestellt&lt;br /&gt;
wurde, gehört zu keiner Sitzung und wird abgewiesen. Nach dem Update müssen sich&lt;br /&gt;
angemeldete Konsumenten also einmalig neu anmelden.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 3: Token erneuern (Refresh)===&lt;br /&gt;
&lt;br /&gt;
Soll die App lange ohne Neuanmeldung arbeiten, stellt das Authenticate-Skript zusätzlich ein Refresh-Token aus (Feld &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039;). Zum Erneuern ruft der Client &#039;&#039;&#039;denselben JWT-Endpunkt&#039;&#039;&#039; auf, aber mit dem Refresh-Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 POST https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi... (Refresh-Token)&lt;br /&gt;
&lt;br /&gt;
Der Server erkennt am Bearer-Token den Refresh-Fall, verifiziert das Token (Signatur, Ablauf, &#039;&#039;token_use=refresh&#039;&#039;), &#039;&#039;&#039;entwertet es in der Sitzung&#039;&#039;&#039; und ruft erst dann im selben Skript die Methode &#039;&#039;&#039;Refresh&#039;&#039;&#039; auf. Das Skript liefert nur noch die aktuellen Claims zurück - Rotation, Einmalgebrauch und Sperrliste erledigt der Server. Die Antwort ist ein rotiertes Paar &#039;&#039;{&amp;quot;token&amp;quot;,&amp;quot;accessToken&amp;quot;,&amp;quot;refreshToken&amp;quot;,&amp;quot;expiresIn&amp;quot;,&amp;quot;serverTime&amp;quot;}&#039;&#039; in &#039;&#039;&#039;derselben Sitzung&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Eine &#039;&#039;&#039;eigene Sperrtabelle im Skript ist nicht mehr nötig&#039;&#039;&#039; und sollte&lt;br /&gt;
entfernt werden. Wer sie weiterführt, rotiert zweimal: einmal im Skript, einmal im&lt;br /&gt;
Server. Der Server sieht die Skript-Tabelle nicht, und das Skript sieht die&lt;br /&gt;
Sitzung nicht.}}&lt;br /&gt;
&lt;br /&gt;
Verhalten bei einem bereits benutzten Refresh-Token:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Lage !! Antwort !! Wirkung auf die Sitzung&lt;br /&gt;
|-&lt;br /&gt;
| Erneute Vorlage &#039;&#039;&#039;innerhalb von 60 Sekunden&#039;&#039;&#039; || 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; || Keine. Das ist der Normalfall einer App, deren parallele Anfragen gleichzeitig in den Refresh laufen - eine gewinnt, die anderen sollen die vom Gewinner gespeicherten Token benutzen&lt;br /&gt;
|-&lt;br /&gt;
| Erneute Vorlage &#039;&#039;&#039;später&#039;&#039;&#039; || 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; || Die &#039;&#039;&#039;gesamte Sitzung wird gesperrt&#039;&#039;&#039;. Das Token wurde kopiert oder wiederholt, ohne dass ein Gewinner es noch brauchen könnte; der Vorfall wird mit IP protokolliert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Details und ein vollständiges Beispiel siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
===Phase 4: Abmelden (Logout)===&lt;br /&gt;
&lt;br /&gt;
Abgemeldet wird mit einem &#039;&#039;&#039;DELETE&#039;&#039;&#039; auf denselben JWT-Endpunkt, das&lt;br /&gt;
Access-Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 DELETE https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi... (Access-Token)&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 204 No Content&lt;br /&gt;
&lt;br /&gt;
Der Server sperrt die &#039;&#039;&#039;ganze Sitzung&#039;&#039;&#039; - also auch die noch nicht&lt;br /&gt;
abgelaufenen Access-Token aller vorherigen Erneuerungen. &#039;&#039;&#039;Ein Skript ist dafür&lt;br /&gt;
nicht nötig&#039;&#039;&#039;: es gibt keine Methode &#039;&#039;Logout&#039;&#039;, der Vorgang läuft komplett in&lt;br /&gt;
der Engine. Damit funktioniert das Abmelden für jeden Zugang mit aktiver&lt;br /&gt;
JWT-Pflicht, ohne dass am Authentifizierungs-Skript etwas geändert wird.&lt;br /&gt;
&lt;br /&gt;
* Ein wiederholter Aufruf ist unschädlich und bleibt &#039;&#039;&#039;204&#039;&#039;&#039; - auch wenn die Sitzung längst gesperrt ist.&lt;br /&gt;
* Ein abgelaufenes oder ungültiges Token ergibt &#039;&#039;&#039;401&#039;&#039;&#039; (es wird schon bei der Prüfung abgewiesen).&lt;br /&gt;
* Ein Refresh-Token statt des Access-Tokens ergibt &#039;&#039;&#039;401&#039;&#039;&#039;.&lt;br /&gt;
* Lässt sich die Sitzung nicht sperren, antwortet der Server &#039;&#039;&#039;503&#039;&#039;&#039; mit &#039;&#039;Retry-After&#039;&#039; - ein Client darf nicht glauben, er sei abgemeldet, während sein Token weiterläuft.&lt;br /&gt;
&lt;br /&gt;
====Alle Geräte abmelden====&lt;br /&gt;
&lt;br /&gt;
Der Fall „Gerät verloren&amp;quot; ist bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; am JWT-Endpunkt untergebracht:&lt;br /&gt;
er braucht eine fachliche Entscheidung darüber, wer das darf. Dafür gibt es das&lt;br /&gt;
&#039;&#039;oWriter.JwtRevoke&#039;&#039;, das jeder gewöhnliche Endpunkt aufrufen kann:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    // &#039;session&#039; = nur diese Sitzung, &#039;all&#039; = alle Sitzungen des Subjects&lt;br /&gt;
    oWriter.JwtRevoke(&#039;all&#039;);&lt;br /&gt;
    oWriter.Status(204);&lt;br /&gt;
    oWriter.NoBody();&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;all&#039;&#039; setzt voraus, dass das Authenticate-Skript &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039;&lt;br /&gt;
gesetzt hat. Ohne Subject lässt sich die Menge der Sitzungen nicht bestimmen, und&lt;br /&gt;
der Aufruf antwortet mit 503.}}&lt;br /&gt;
&lt;br /&gt;
==Woher ein Anmelder seine Identität bekommt==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Diese Frage entscheidet das Skript des Zugangs, nicht der Server.&#039;&#039;&#039; Das&lt;br /&gt;
Authentifizierungs-Skript liegt je Zugang in &amp;lt;code&amp;gt;ra_jwt_script&amp;lt;/code&amp;gt; - und&lt;br /&gt;
damit gilt für jede Anwendung ihre eigene Regel.&lt;br /&gt;
&lt;br /&gt;
Das ist keine Kleinigkeit, sondern der Punkt, an dem sich zwei Anwendungen&lt;br /&gt;
unterscheiden. Ein Beispiel aus dem Betrieb: Ein Zugang für eine&lt;br /&gt;
Techniker-Anwendung lehnt jeden Benutzer ohne Mitarbeiterzuordnung&lt;br /&gt;
(&amp;lt;code&amp;gt;BENUTZ.b_mitarbeiter&amp;lt;/code&amp;gt;) ab - &#039;&#039;&#039;bewusst&#039;&#039;&#039;, denn ein Techniker ohne&lt;br /&gt;
Zuordnung bekäme eine leere Auftragsliste und würde den Fehler bei sich suchen.&lt;br /&gt;
Für einen Kundenzugang wäre genau dieselbe Regel falsch: sie träfe &#039;&#039;&#039;jeden&#039;&#039;&#039;&lt;br /&gt;
Anmelder.&lt;br /&gt;
&lt;br /&gt;
Wer einen zweiten Zugang für eine andere Anwendung anlegt, prüft deshalb als&lt;br /&gt;
Erstes: Woraus besteht die Identität dieser Anmelder, und welche Ablehnung ist&lt;br /&gt;
für sie richtig? Ein kopiertes Skript bringt die Regel der anderen Anwendung&lt;br /&gt;
mit, und sie fällt erst auf, wenn sich der erste echte Anmelder nicht anmelden&lt;br /&gt;
kann.&lt;br /&gt;
&lt;br /&gt;
==Sitzungen (RESTSRV_TOKEN)==&lt;br /&gt;
&lt;br /&gt;
Je ausgestelltem Token-Paar entsteht eine Zeile. Sie enthält die Sitzung, die&lt;br /&gt;
Zeilenkennung, das Subject, die beiden Laufzeiten, IP und Trace-ID der Ausstellung&lt;br /&gt;
sowie die Kennzeichen „verbraucht&amp;quot; und „gesperrt&amp;quot;. Schickt ein Reverse-Proxy den Header&lt;br /&gt;
&#039;&#039;X-Forwarded-For&#039;&#039; mit, steht dessen Wert zusätzlich in &#039;&#039;rt_ip_fwd&#039;&#039;; &#039;&#039;rt_ip&#039;&#039; bleibt&lt;br /&gt;
die IP-Adresse der Verbindung.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Rotation und Widerruf sind getrennt.&#039;&#039;&#039; Ein Refresh entwertet nur das Refresh-Token; der zugehörige Access-Token bleibt bis zu seinem Ablauf gültig. Sonst bekämen parallel laufende Anfragen ein 401, obwohl niemand abgemeldet ist.&lt;br /&gt;
* &#039;&#039;&#039;Die Sitzung überlebt die Rotation.&#039;&#039;&#039; Alle Paare einer Anmeldung tragen dieselbe Sitzungskennung - deshalb kann ein Logout sie gemeinsam sperren.&lt;br /&gt;
* &#039;&#039;&#039;Aufräumen läuft automatisch&#039;&#039;&#039; beim Serverstart und danach höchstens stündlich. Gelöscht wird nur, was nichts mehr entscheiden kann: Zeilen, deren Access- &#039;&#039;&#039;und&#039;&#039;&#039; Refresh-Laufzeit vorbei sind. Eine verbrauchte Zeile bleibt bis dahin stehen, weil sonst die Erkennung eines später vorgelegten gestohlenen Refresh-Tokens verloren ginge.&lt;br /&gt;
* &#039;&#039;&#039;Kein Eingriff nötig.&#039;&#039;&#039; Die Tabelle wird nicht gepflegt; sie ist Diagnose-Material. Alle Abweisungen stehen als &#039;&#039;TokenStore: ...&#039;&#039; in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==mTLS-Subject-Prüfung==&lt;br /&gt;
&lt;br /&gt;
Wird im Feld &#039;&#039;&#039;mTLS-Subject&#039;&#039;&#039; ein Wert hinterlegt, prüft der Server zusätzlich zur Standard-mTLS-Validierung, ob der Subject-DN des (CA-validierten) Client-Zertifikats die hinterlegten &#039;&#039;&#039;RDN-Komponenten&#039;&#039;&#039; enthält. Verglichen wird jede Komponente &#039;&#039;&#039;vollständig&#039;&#039;&#039; (kein Substring-Match) und case-insensitiv; mehrere Komponenten werden durch Komma oder Semikolon getrennt und müssen &#039;&#039;&#039;alle&#039;&#039;&#039; im Subject vorkommen.&lt;br /&gt;
&lt;br /&gt;
Beispiele für Subject-Strings im Cert (OneLine-DN):&lt;br /&gt;
&lt;br /&gt;
 /C=DE/O=Kunde GmbH/CN=client-prod.kunde.de&lt;br /&gt;
 /C=DE/O=Kunde GmbH/CN=client-test.kunde.de&lt;br /&gt;
&lt;br /&gt;
Mit &#039;&#039;&#039;CN=client-prod.kunde.de&#039;&#039;&#039; im Feld wird genau das prod-Cert akzeptiert, das test-Cert mit 401 abgewiesen. Da jede RDN-Komponente vollständig verglichen wird, matcht z.B. &#039;&#039;&#039;CN=client1&#039;&#039;&#039; nicht fälschlich auch &#039;&#039;&#039;CN=client10&#039;&#039;&#039;. Mehrere Bedingungen lassen sich mit Komma/Semikolon kombinieren, z.B. &#039;&#039;O=Kunde GmbH;CN=client-prod.kunde.de&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==Listen-Funktionen==&lt;br /&gt;
&lt;br /&gt;
In der Zugänge-Liste (allein):&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfg&#039;&#039;&#039; - neuer Zugang&lt;br /&gt;
* &#039;&#039;&#039;Return&#039;&#039;&#039; - Zugang bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F4&#039;&#039;&#039; - Sortierung&lt;br /&gt;
* &#039;&#039;&#039;F7&#039;&#039;&#039; - JWT-Authentifizierungs-Skript bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F8&#039;&#039;&#039; - Statistik für den Zugang&lt;br /&gt;
&lt;br /&gt;
Aus der Endpunkt-Liste via F6 geöffnet (Auswahl für Berechtigung):&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;F2&#039;&#039;&#039; - markierte Zugänge dem Endpunkt zuordnen&lt;br /&gt;
* &#039;&#039;&#039;F5&#039;&#039;&#039; - Zugang in der Liste markieren / Markierung aufheben&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer&amp;diff=67055</id>
		<title>OBS/Kostenpflichtige Module/RESTServer</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer&amp;diff=67055"/>
		<updated>2026-10-26T08:22:17Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Protokoll: Hinweis auf X-Forwarded-For hinter einem Reverse-Proxy ergänzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
=REST-Server=&lt;br /&gt;
{{Hinweis|Es handelt sich um ein kostenpflichtiges Modul. Die Schnittstelle muss über den OBS-Support aktiviert werden.}}&lt;br /&gt;
&lt;br /&gt;
==Was leistet der REST-Server?==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server ist die &#039;&#039;&#039;universelle Schnittstelle&#039;&#039;&#039; Ihrer OBS-Installation nach außen. Er macht aus Ihrem OBS einen Dienst, mit dem andere Programme - Webseiten, Apps, Geräte, Kundensysteme, Cloud-Dienste - direkt sprechen können. Statt Daten manuell zu exportieren, zu mailen oder über Umwege bereitzustellen, holen sich angebundene Systeme genau die Information, die sie brauchen, in dem Moment, in dem sie sie brauchen - oder liefern neue Daten direkt in OBS ab.&lt;br /&gt;
&lt;br /&gt;
Für Anwender, die nicht selbst programmieren, bedeutet das: &#039;&#039;&#039;Was bisher nur manuell, per Datei-Import oder über Spezialschnittstellen ging, lässt sich jetzt automatisieren.&#039;&#039;&#039; Eine Webseite zeigt live aktuelle Lagerbestände. Mitarbeiter erfassen Zeiten über das Handy, ohne dass jemand Excel-Listen einpflegen muss. Ein Kunde stößt per Bestelltaste in seinem System einen Vorgang in Ihrem OBS an. Ein Lieferant meldet Wareneingänge automatisch zurück. Jeder dieser Anwendungsfälle wird einmal eingerichtet und läuft danach automatisch.&lt;br /&gt;
&lt;br /&gt;
Technisch gesehen ist der REST-Server ein in OBS integrierter, voll konfigurierbarer &#039;&#039;&#039;HTTP/HTTPS-Dienst&#039;&#039;&#039;, dessen Endpunkte über in OBS gepflegte Pascal-Skripte realisiert werden. Jeder Endpunkt bekommt eine eigene Adresse, eine eigene Logik und eine eigene Berechtigungsstruktur. Rückgaben erfolgen ausschliesslich im JSON-Format. Damit lassen sich beliebige Lese- und Schreibvorgänge auf der OBS-Datenbank realisieren, ohne dass externe Systeme direkten Datenbank-Zugriff erhalten - das ganze OBS-Regelwerk (Rechte, Validierung, Geschäftslogik) bleibt aktiv.&lt;br /&gt;
&lt;br /&gt;
Im Ergebnis ist der REST-Server kein einzelnes Feature, sondern ein &#039;&#039;&#039;Werkzeugkasten&#039;&#039;&#039;: Was an Daten oder Funktionen in OBS verfügbar ist, kann über den REST-Server auch nach außen angeboten - oder von außen entgegengenommen - werden. Die Bandbreite reicht vom einfachen Lese-Endpunkt für ein Webseiten-Widget bis hin zu kompletten B2B-Integrationen mit Mandanten- und Rollen-Trennung.&lt;br /&gt;
&lt;br /&gt;
Aufruf des Moduls in OBS:&lt;br /&gt;
&#039;&#039;&#039;Stammdaten&#039;&#039;&#039; -&amp;gt; &#039;&#039;&#039;Z Weitere Stammdaten&#039;&#039;&#039; -&amp;gt; &#039;&#039;&#039;REST-Server&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Anwendungsbereiche==&lt;br /&gt;
&lt;br /&gt;
Die folgenden Einsatzfälle sind typische Beispiele, an denen sich der praktische Nutzen ablesen lässt. Es ist keine abschliessende Liste - alles, was sich in OBS-Skripten abbilden lässt, kann auch über einen Endpunkt bereitgestellt werden.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Dies sind Beispiele. Für die konkrete Entwicklung können, je nach Komplexität, weitere Kosten anfallen.}}&lt;br /&gt;
&lt;br /&gt;
===Webseiten und Kundenportale===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Live-Daten auf der eigenen Webseite:&#039;&#039;&#039; Lagerbestände, Verfügbarkeiten, Preise, Auftragsstatus aus OBS direkt anzeigen, ohne tägliche Datei-Exports.&lt;br /&gt;
* &#039;&#039;&#039;Kundenportale:&#039;&#039;&#039; Kunden sehen ihre offenen Posten, Auftragshistorie, Lieferscheine. Die Seite holt sich die Daten zur Anzeigezeit direkt aus OBS, jeder Kunde sieht nur seine eigenen Daten.&lt;br /&gt;
* &#039;&#039;&#039;Trackingseiten:&#039;&#039;&#039; Status einer Bestellung, eines Auftrags oder einer Reparatur für Endkunden, abrufbar über eine Sendungsnummer.&lt;br /&gt;
&lt;br /&gt;
===Mobile Anwendungen und Außendienst===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Mobile Zeiterfassung:&#039;&#039;&#039; Mitarbeiter buchen Anfang, Pause und Ende über Smartphone oder Tablet, die Daten landen direkt in OBS - ohne Zettelwirtschaft.&lt;br /&gt;
* &#039;&#039;&#039;QR-/Barcode-Scanner und Lagergeräte:&#039;&#039;&#039; Wareneingang, Inventur, Kommissionierung direkt in OBS abbilden, ohne zwischengeschaltete Software.&lt;br /&gt;
* &#039;&#039;&#039;Außendienst-Apps:&#039;&#039;&#039; Techniker sehen ihre Aufträge, dokumentieren Einsätze, erfassen Material - synchronisiert mit OBS.&lt;br /&gt;
&lt;br /&gt;
===Anbindung von Drittsystemen===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Webshops:&#039;&#039;&#039; Bestellungen werden direkt vom Shop in OBS gemeldet, Lagerbestände und Preise vom Shop bei OBS abgefragt.&lt;br /&gt;
* &#039;&#039;&#039;Buchhaltungs- und ERP-Systeme:&#039;&#039;&#039; Bidirektionaler Austausch von Belegen, Stammdaten und Buchungen.&lt;br /&gt;
* &#039;&#039;&#039;CRM- und Marketing-Tools:&#039;&#039;&#039; Personendaten, Aktivitäten oder Mailing-Empfänger werden synchron gehalten.&lt;br /&gt;
* &#039;&#039;&#039;Logistikdienstleister:&#039;&#039;&#039; Versandaufträge werden übergeben, Tracking-Informationen zurückgeliefert.&lt;br /&gt;
&lt;br /&gt;
===Automatisierte Rückmeldungen (WebHooks)===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Zahlungs-Callbacks:&#039;&#039;&#039; Bezahldienste (z.B. PayPal, Klarna, Stripe) melden den Zahlungseingang an einen WebHook-Endpunkt - OBS bucht automatisch.&lt;br /&gt;
* &#039;&#039;&#039;Versanddienste:&#039;&#039;&#039; Statusmeldungen (verschickt, zugestellt, retour) werden direkt im passenden Vorgang dokumentiert.&lt;br /&gt;
* &#039;&#039;&#039;Cloud-Workflows:&#039;&#039;&#039; Externe Automatisierungs-Plattformen (Make, Zapier, n8n, ...) stoßen OBS-Vorgänge an oder werden von OBS aus angesprochen.&lt;br /&gt;
&lt;br /&gt;
===Geschäftspartner-Kommunikation (B2B)===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lieferanten-Schnittstelle:&#039;&#039;&#039; Bestellungen werden automatisch übergeben, Auftragsbestätigungen und Lieferavise zurückgespielt.&lt;br /&gt;
* &#039;&#039;&#039;Kunden-Schnittstelle:&#039;&#039;&#039; Grosskunden bestellen direkt aus ihrem ERP heraus, Stammdaten und Konditionen werden zentral gepflegt.&lt;br /&gt;
* &#039;&#039;&#039;Sichere Maschine-zu-Maschine-Kommunikation:&#039;&#039;&#039; Mit Mutual-TLS (mTLS) wird sichergestellt, dass nur die freigeschalteten Partnersysteme - und niemand sonst - mit OBS sprechen können.&lt;br /&gt;
&lt;br /&gt;
===Interne Microservices und Automatisierungen===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Eigene kleine Hilfs-Tools:&#039;&#039;&#039; Zeitschaltautomatik, Reporting-Generator, Sammelaktionen, die aus mehreren Quellen Daten in OBS verarbeiten.&lt;br /&gt;
* &#039;&#039;&#039;Verbindung mehrerer Standorte:&#039;&#039;&#039; Verteilte Systeme tauschen Daten über definierte Endpunkte aus, ohne offene Datenbankverbindungen.&lt;br /&gt;
* &#039;&#039;&#039;Datenbereitstellung für Dashboards und Auswertungen:&#039;&#039;&#039; BI-Tools (Power BI, Grafana, ...) lesen aufbereitete Kennzahlen direkt aus OBS.&lt;br /&gt;
&lt;br /&gt;
===Was bedeutet das wirtschaftlich?===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Manuelle Arbeit fällt weg.&#039;&#039;&#039; Daten werden nicht mehr aus OBS exportiert, per Mail geschickt und woanders eingelesen - sie fliessen direkt.&lt;br /&gt;
* &#039;&#039;&#039;Fehler werden weniger.&#039;&#039;&#039; Jede manuelle Stelle ist eine mögliche Fehlerquelle; jede Automatisierung eine Stelle, an der nichts mehr schiefgehen kann.&lt;br /&gt;
* &#039;&#039;&#039;Neue Geschäftsmodelle werden möglich.&#039;&#039;&#039; Kunden- und Lieferantenportale, Self-Service-Funktionen, App-Anbindungen lassen sich aufbauen, ohne ein zweites System pflegen zu müssen.&lt;br /&gt;
* &#039;&#039;&#039;Bestehende Software bleibt verbunden.&#039;&#039;&#039; Statt eine vorhandene Anwendung ablösen zu müssen, kann sie über den REST-Server an OBS angedockt werden - in beiden Richtungen.&lt;br /&gt;
* &#039;&#039;&#039;Skaliert mit:&#039;&#039;&#039; Was klein anfängt (ein einzelner Endpunkt für eine Webseite) kann zu einer kompletten Integrations-Plattform ausgebaut werden, ohne dass die Grundlage gewechselt werden muss.&lt;br /&gt;
&lt;br /&gt;
==Architektur im Überblick==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server besteht aus mehreren aufeinander aufbauenden Bausteinen, die alle in OBS gepflegt werden:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Baustein     !! Tabelle              !! Zweck&lt;br /&gt;
|-&lt;br /&gt;
| Server       || RESTSRV_SERVER       || TLS-Profil + eigene HTTP-Server-Instanz, kann mehrfach existieren&lt;br /&gt;
|-&lt;br /&gt;
| Bindung      || RESTSRV_BINDINGS     || IP-Adresse + Port pro Server&lt;br /&gt;
|-&lt;br /&gt;
| Zugang       || RESTSRV_ACCOUNT      || API-Key, optionale Host- und CORS-Beschränkung, optionale JWT-Konfiguration&lt;br /&gt;
|-&lt;br /&gt;
| Endpunkt     || RESTSRV_ENDPOINTS    || Skript, das die Anfrage bearbeitet, einem Server fest zugeordnet&lt;br /&gt;
|-&lt;br /&gt;
| Berechtigung || RESTSRV_ACCESS       || Verbindet Zugang und Endpunkt&lt;br /&gt;
|-&lt;br /&gt;
| Idempotenz   || RESTSRV_IDEMPOTENCY  || Speicher für Idempotency-Keys und die dazu gelieferte Antwort&lt;br /&gt;
|-&lt;br /&gt;
| Sitzung      || RESTSRV_TOKEN        || Ausgestellte Token-Paare je Sitzung; Grundlage für Rotation und Widerruf&lt;br /&gt;
|-&lt;br /&gt;
| Protokoll    || RESTSRV_PROTO        || Laufende Ereignisse, Fehler, Audit-Einträge&lt;br /&gt;
|-&lt;br /&gt;
| Statistik    || RESTSRV_STATS        || Aufrufzähler, Antwortzeiten und HTTP-Status pro Endpunkt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Eine Anfrage durchläuft folgende Stationen:&lt;br /&gt;
&lt;br /&gt;
# Rate-Limit-Prüfung (pro API-Key bzw. IP und global, Schwellen je Server-Profil)&lt;br /&gt;
# Andrangsgrenze des Server-Profils (&#039;&#039;Max. parallel&#039;&#039;) - bei Überschreitung &#039;&#039;&#039;503&#039;&#039;&#039; mit &#039;&#039;Retry-After&#039;&#039;&lt;br /&gt;
# Authentifizierung über den API-Key (Header &#039;&#039;apikey&#039;&#039;)&lt;br /&gt;
# CORS-Prüfung (sofern &#039;&#039;Origin&#039;&#039;-Header gesetzt)&lt;br /&gt;
# Optional: JWT-Prüfung - Signatur, Ablauf und Sitzung (sofern für den Zugang aktiviert)&lt;br /&gt;
# Routing: Auflösung des Pfads über die Pfad-Templates der Endpunkte&lt;br /&gt;
# Prüfung der Berechtigung (Zugang -&amp;gt; Endpunkt)&lt;br /&gt;
# Entpacken des Anfragekörpers, sofern er mit &#039;&#039;Content-Encoding: gzip&#039;&#039; gesendet wurde&lt;br /&gt;
# Idempotenz-Prüfung bei POST/PUT/PATCH/DELETE - der Header &#039;&#039;Idempotency-Key&#039;&#039; ist dort &#039;&#039;&#039;Pflicht&#039;&#039;&#039;&lt;br /&gt;
# Ausführung des Endpunkt-Skripts (oder WebHook-Antwort)&lt;br /&gt;
# Festschreiben des Ergebnisses im Idempotenz-Speicher (sofern in Schritt 9 reserviert)&lt;br /&gt;
# JSON-Antwort und Statistik-Eintrag&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Die Reihenfolge ist kein Zufall.&#039;&#039;&#039; Das Entpacken liegt &#039;&#039;&#039;hinter&#039;&#039;&#039;&lt;br /&gt;
der Authentifizierung, damit ein Unbefugter dem Server keine Rechenzeit für eine&lt;br /&gt;
Dekompressionsbombe abverlangen kann - und &#039;&#039;&#039;vor&#039;&#039;&#039; der Idempotenz-Reservierung,&lt;br /&gt;
damit eine abgewiesene Anfrage keinen &#039;&#039;Idempotency-Key&#039;&#039; verbrennt.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Der Normalfall erzeugt keine Protokollzeile.&#039;&#039;&#039; Bis 08/2026 schrieb jede&lt;br /&gt;
Anfrage eine Zeile nach RESTSRV_PROTO; sie ist entfallen, weil RESTSRV_STATS&lt;br /&gt;
zu derselben Anfrage IP, Zugang, Endpunkt, Methode, Pfad, Status, Laufzeit und&lt;br /&gt;
dieselbe Korrelations-ID trägt - und länger aufbewahrt wird. Das Protokoll trägt&lt;br /&gt;
seitdem nur noch Auffälligkeiten.}}&lt;br /&gt;
&lt;br /&gt;
==Adressierung==&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt wird über ein &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; angesprochen, das den Pfad nach dem Host beschreibt und Platzhalter enthalten kann:&lt;br /&gt;
&lt;br /&gt;
 http://[Hostadresse][:Port][Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Beispiele:&lt;br /&gt;
&lt;br /&gt;
 https://api.meinserver.de/kalender/v1&lt;br /&gt;
 https://api.meinserver.de/orders/{uid}&lt;br /&gt;
 https://api.meinserver.de/orders/{uid}/modules/{code}&lt;br /&gt;
&lt;br /&gt;
Statische Segmente müssen exakt passen; Platzhalter in geschweiften Klammern (z.B. {uid}) matchen einen beliebigen Wert und stehen im Skript als Pfad-Parameter zur Verfügung. Eine Versionskennung wird als statisches Segment ins Template aufgenommen (z.B. /kalender/v1), um eine Funktionalität zu verändern, ohne bestehende Konsumenten zu brechen. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].&lt;br /&gt;
&lt;br /&gt;
==Unterstützte HTTP-Methoden==&lt;br /&gt;
&lt;br /&gt;
Der Server akzeptiert die Methoden &#039;&#039;&#039;GET&#039;&#039;&#039;, &#039;&#039;&#039;POST&#039;&#039;&#039;, &#039;&#039;&#039;PUT&#039;&#039;&#039;, &#039;&#039;&#039;DELETE&#039;&#039;&#039; und &#039;&#039;&#039;PATCH&#039;&#039;&#039; sowie &#039;&#039;&#039;OPTIONS&#039;&#039;&#039; (CORS-Preflight, ohne Skript-Aufruf). Jede Methode wird im Endpunkt-Skript als gleichnamige Prozedur implementiert - &#039;&#039;&#039;mit einer Ausnahme&#039;&#039;&#039;: &#039;&#039;Delete&#039;&#039; ist in der Skriptsprache ein reservierter Name und lässt sich nicht als Prozedur deklarieren. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
Fehlt die zur Methode passende Prozedur, antwortet der Server mit &#039;&#039;&#039;405&#039;&#039;&#039; und&lt;br /&gt;
nennt im Header &#039;&#039;Allow&#039;&#039; die Verben, die dieses Skript tatsächlich anbietet.&lt;br /&gt;
&lt;br /&gt;
==Kompression==&lt;br /&gt;
&lt;br /&gt;
Der Server packt Antworten mit &#039;&#039;&#039;gzip&#039;&#039;&#039;, wenn der Client sie annimmt. Ausgehandelt wird über den Anfrage-Header &amp;lt;code&amp;gt;Accept-Encoding&amp;lt;/code&amp;gt;; die Antwort trägt dann &amp;lt;code&amp;gt;Content-Encoding: gzip&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;Vary: Accept-Encoding&amp;lt;/code&amp;gt; - bei einer CORS-Anfrage steht dort &amp;lt;code&amp;gt;Vary: Origin, Accept-Encoding&amp;lt;/code&amp;gt;, also &#039;&#039;&#039;eine&#039;&#039;&#039; Zeile mit beiden Bedingungen. Browser, &amp;lt;code&amp;gt;curl --compressed&amp;lt;/code&amp;gt; und die gängigen HTTP-Bibliotheken entpacken selbstständig - für den Konsumenten ändert sich nichts ausser der übertragenen Datenmenge.&lt;br /&gt;
&lt;br /&gt;
Gepackt wird nur, wo es etwas bringt:&lt;br /&gt;
&lt;br /&gt;
* Nur JSON (auch &#039;&#039;problem+json&#039;&#039; und &#039;&#039;ndjson&#039;&#039;), XML, JavaScript, SVG und alles unter &#039;&#039;text/&#039;&#039; - darunter Text, CSV und HTML. Bereits komprimierte Typen - PDF, Bild, ZIP, Video - werden durch gzip &#039;&#039;&#039;grösser&#039;&#039;&#039;, nicht kleiner.&lt;br /&gt;
* Erst ab &#039;&#039;&#039;1024 Byte&#039;&#039;&#039;. Darunter kostet der gzip-Rahmen mehr, als der Inhalt einspart.&lt;br /&gt;
* &#039;&#039;&#039;Nicht&#039;&#039;&#039; bei Dateiantworten, nicht bei Teilauslieferungen (206) und nicht bei Antworten ohne Körper (204, 304).&lt;br /&gt;
* Nicht, wenn das Endpunkt-Skript den Header &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; selbst gesetzt hat - dann gehört die Antwort dem Skript.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;deflate&#039;&#039;&#039; wird in &#039;&#039;&#039;keiner&#039;&#039;&#039; Richtung unterstützt. Der Wert ist historisch uneindeutig - die einen senden RFC-1950-gerahmt, die anderen roh -, und wer ihn nennt, beherrscht ohnehin gzip. Eine Anfrage mit &amp;lt;code&amp;gt;Content-Encoding: deflate&amp;lt;/code&amp;gt; wird mit 415 abgewiesen.&lt;br /&gt;
&lt;br /&gt;
Abschalten lässt sich das Packen je Server-Profil über den Haken &#039;&#039;Keine Kompression&#039;&#039; (siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]). Der Haken betrifft ausschliesslich die Antwortrichtung.&lt;br /&gt;
&lt;br /&gt;
===Komprimierte Anfragen===&lt;br /&gt;
&lt;br /&gt;
In der Gegenrichtung nimmt der Server &#039;&#039;&#039;gzip&#039;&#039;&#039; entgegen: Trägt eine Anfrage den Header &amp;lt;code&amp;gt;Content-Encoding: gzip&amp;lt;/code&amp;gt;, wird der Körper entpackt, bevor ihn jemand liest. Das Endpunkt-Skript merkt davon nichts.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Anfrage                                              !! Antwort&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; fehlt oder &amp;lt;code&amp;gt;identity&amp;lt;/code&amp;gt; || normale Verarbeitung; ab 10 MB Körpergrösse &#039;&#039;&#039;413&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;gzip&amp;lt;/code&amp;gt;                                    || Körper wird entpackt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;deflate&amp;lt;/code&amp;gt;, anderer Wert (z.B. &amp;lt;code&amp;gt;br&amp;lt;/code&amp;gt;) oder mehrere Werte || &#039;&#039;&#039;415&#039;&#039;&#039; &amp;lt;code&amp;gt;UNSUPPORTED_MEDIA_TYPE&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Gepackter Körper bei einem Datei-Upload              || &#039;&#039;&#039;415&#039;&#039;&#039; - siehe [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]]&lt;br /&gt;
|-&lt;br /&gt;
| Entpackter Körper über &#039;&#039;&#039;10 MB&#039;&#039;&#039;                    || &#039;&#039;&#039;413&#039;&#039;&#039; &amp;lt;code&amp;gt;PAYLOAD_TOO_LARGE&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Entpackter Körper über dem &#039;&#039;&#039;400fachen&#039;&#039;&#039; der gesendeten Grösse || &#039;&#039;&#039;413&#039;&#039;&#039; &amp;lt;code&amp;gt;PAYLOAD_TOO_LARGE&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Für einen gepackten Anfragekörper gelten &#039;&#039;&#039;zwei&#039;&#039;&#039; Grenzen. Beide zählen den &#039;&#039;&#039;entpackten&#039;&#039;&#039; Inhalt, und beide greifen bereits &#039;&#039;&#039;während&#039;&#039;&#039; des Entpackens - ein Körper, der sie reisst, belegt den Speicher gar nicht erst:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;10 MB&#039;&#039;&#039; absolut. Dieselbe Grenze, die auch für einen ungepackten Körper gilt.&lt;br /&gt;
* &#039;&#039;&#039;das 400fache&#039;&#039;&#039; der gesendeten Grösse, &#039;&#039;&#039;mindestens aber 64 KB&#039;&#039;&#039;. Damit wird eine &#039;&#039;Dekompressionsbombe&#039;&#039; - wenige Kilobyte, die sich auf Hunderte Megabyte aufblähen - schon nach wenigen Prozent abgebrochen. Der Boden von 64 KB sorgt dafür, dass die Verhältnisrechnung winzige Körper gar nicht erst trifft: ein 100-Byte-Körper darf sich auf 64 KB entpacken, nicht nur auf 40 KB.&lt;br /&gt;
&lt;br /&gt;
Die zweite Grenze trifft normale Nutzlasten nicht. Gemessen erreichen echte JSON-Körper Verhältnisse von 24:1 bis 40:1, und selbst eine Liste aus zehntausenden identischen Zeilen kommt nur auf rund 340:1 - mehr gibt das Verfahren für wiederholte Datensätze nicht her. Über 400:1 kommt nur, wer sehr lange Läufe desselben Zeichens sendet.&lt;br /&gt;
&lt;br /&gt;
Für Datei-Uploads gilt keine der beiden Grenzen - dort zählt allein die je Endpunkt eingestellte Maximalgrösse.}}&lt;br /&gt;
&lt;br /&gt;
==Persistente Verbindungen (Keep-alive)==&lt;br /&gt;
&lt;br /&gt;
Der Server hält eine Verbindung nach der Antwort &#039;&#039;&#039;offen&#039;&#039;&#039;, statt sie zu&lt;br /&gt;
schliessen. Ein Client, der mehrere Anfragen hintereinander stellt - eine App,&lt;br /&gt;
die eine Liste seitenweise abholt -, spart damit je Folgeanfrage den Aufbau von&lt;br /&gt;
TCP-Verbindung und TLS-Sitzung.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Grenze !! Wert !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| Leerlauf || &#039;&#039;&#039;10 Sekunden&#039;&#039;&#039; || Passiert auf der Verbindung so lange nichts, wird sie geschlossen&lt;br /&gt;
|-&lt;br /&gt;
| Anfragen je Verbindung || &#039;&#039;&#039;100&#039;&#039;&#039; || Danach schliesst der Server geordnet, der Client baut neu auf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beides ist für den Konsumenten unkritisch: Eine geschlossene Verbindung ist kein&lt;br /&gt;
Fehler, jede HTTP-Bibliothek baut sie bei der nächsten Anfrage selbst wieder auf.&lt;br /&gt;
Wer viele Anfragen hintereinander stellt, gewinnt aber deutlich, wenn er seinen&lt;br /&gt;
Client die Verbindung wiederverwenden lässt (&#039;&#039;Connection: keep-alive&#039;&#039; ist der&lt;br /&gt;
Standard von HTTP/1.1, die meisten Bibliotheken tun es von allein).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Die Leerlaufgrenze gilt für jeden Lesevorgang der Verbindung&#039;&#039;&#039;,&lt;br /&gt;
nicht nur für das Warten auf die nächste Anfrage. Ein Client, der seinen&lt;br /&gt;
Anfragekörper nur tropfenweise sendet und dazwischen länger als 10 Sekunden&lt;br /&gt;
pausiert, verliert die Verbindung mitten im Senden.}}&lt;br /&gt;
&lt;br /&gt;
Die Werte sind bewusst knapp gewählt: Der Server fährt einen eigenen Thread je&lt;br /&gt;
Verbindung, eine leerlaufende Verbindung kostet also nicht bloss einen&lt;br /&gt;
Dateizeiger. Sie sind keine Lastgrenze - dafür gibt es &#039;&#039;Max. Verbindungen&#039;&#039; und&lt;br /&gt;
&#039;&#039;Max. parallel&#039;&#039; am Server-Profil (siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]).&lt;br /&gt;
&lt;br /&gt;
==Datei-Uploads==&lt;br /&gt;
&lt;br /&gt;
Endpunkte, die dafür freigeschaltet sind (&amp;lt;code&amp;gt;re_upload&amp;lt;/code&amp;gt;), nehmen Dateien&lt;br /&gt;
per &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; oder als fortsetzbaren &#039;&#039;&#039;Resumable-Upload&#039;&#039;&#039;&lt;br /&gt;
(&amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;) entgegen. Die maximale Grösse wird pro Endpunkt&lt;br /&gt;
festgelegt (&amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;, Standard 25&amp;amp;nbsp;MB); Überschreitung&lt;br /&gt;
ergibt 413, ein Upload an einen nicht freigeschalteten Endpunkt 415. Der Server&lt;br /&gt;
legt die Datei temporär ab und übergibt dem Skript den Pfad; die weitere&lt;br /&gt;
Verarbeitung (z.B. DMS-Ablage) übernimmt das Skript. Details:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
==Doppelte Sendungen abfangen (Idempotenz)==&lt;br /&gt;
&lt;br /&gt;
Ein Client, der nach einem Timeout dieselbe Anfrage erneut schickt, darf keine&lt;br /&gt;
Zweitbuchung auslösen. Der Server erledigt das selbst: Jeder Aufruf mit&lt;br /&gt;
&#039;&#039;&#039;POST&#039;&#039;&#039;, &#039;&#039;&#039;PUT&#039;&#039;&#039;, &#039;&#039;&#039;PATCH&#039;&#039;&#039; oder &#039;&#039;&#039;DELETE&#039;&#039;&#039; muss den Header&lt;br /&gt;
&amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; tragen; der Server merkt sich Schlüssel und Ergebnis&lt;br /&gt;
in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;. Eine Wiederholung mit demselben Schlüssel und&lt;br /&gt;
demselben Inhalt bekommt die &#039;&#039;&#039;gespeicherte Antwort&#039;&#039;&#039; zurück - das&lt;br /&gt;
Endpunkt-Skript läuft gar nicht erst an.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Der Header ist Pflicht, nicht optional.&#039;&#039;&#039; Ein schreibender Aufruf&lt;br /&gt;
&#039;&#039;&#039;ohne&#039;&#039;&#039; &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; wird mit &#039;&#039;&#039;400&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt; abgewiesen, und das Endpunkt-Skript läuft&lt;br /&gt;
nicht an.&lt;br /&gt;
&lt;br /&gt;
Das war bis v1.21 anders: fehlte der Header, lief die Anfrage ungeschützt durch.&lt;br /&gt;
Der Schutz gegen eine Doppelbuchung stand damit in der Zusage des Clients und&lt;br /&gt;
nicht in einer Prüfung des Servers - ein Wiederholversuch nach Zeitüberschreitung&lt;br /&gt;
buchte ein zweites Mal. Konsumenten, die bisher ohne den Header schreiben, müssen&lt;br /&gt;
angepasst werden.}}&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Pflicht nur für schreibende Verben.&#039;&#039;&#039; &#039;&#039;GET&#039;&#039; braucht keinen Schlüssel und kennt die Mechanik nicht.&lt;br /&gt;
* &#039;&#039;&#039;Der JWT-Endpunkt ist ausgenommen.&#039;&#039;&#039; Anmelden, Erneuern und Abmelden laufen vor der Adressauflösung und brauchen keinen Schlüssel.&lt;br /&gt;
* &#039;&#039;&#039;Kein Schalter am Endpunkt.&#039;&#039;&#039; Die Mechanik greift für jeden Endpunkt automatisch.&lt;br /&gt;
* &#039;&#039;&#039;Das Skript muss nichts tun.&#039;&#039;&#039; Eine eigene Idempotenz-Logik im Skript ist nicht nötig.&lt;br /&gt;
* &#039;&#039;&#039;Pro Vorgang ein Schlüssel&#039;&#039;&#039;, über alle Wiederholversuche hinweg unverändert - nicht bei jedem Versuch neu erzeugt.&lt;br /&gt;
&lt;br /&gt;
Details und die Statuscodes: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]], Abschnitt Idempotenz.&lt;br /&gt;
&lt;br /&gt;
==Sitzungen und Abmelden==&lt;br /&gt;
&lt;br /&gt;
Ein JWT trägt seine Gültigkeit in sich: der Server prüft Signatur und Ablauf und&lt;br /&gt;
braucht dafür keine Datenbank. Genau deshalb liesse sich ein ausgegebener Token&lt;br /&gt;
von sich aus auch nicht mehr zurückziehen - ein verlorenes Gerät wäre bis zum&lt;br /&gt;
Ablauf des Tokens weiter nutzbar.&lt;br /&gt;
&lt;br /&gt;
Der Server führt deshalb zu jedem ausgestellten Token-Paar eine Zeile in&lt;br /&gt;
&#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; und prüft jeden Zugriff dagegen. Daraus ergeben sich drei&lt;br /&gt;
Eigenschaften:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Abmelden wirkt sofort.&#039;&#039;&#039; Ein &#039;&#039;DELETE&#039;&#039; auf den JWT-Endpunkt sperrt die Sitzung; alle Token dieser Sitzung sind damit unmittelbar ungültig, auch die noch nicht abgelaufenen. Anmelden, Erneuern und Abmelden laufen also über einen einzigen Endpunkt, und für das Abmelden ist kein Skript nötig.&lt;br /&gt;
* &#039;&#039;&#039;Ein Refresh-Token gilt genau einmal.&#039;&#039;&#039; Beim Erneuern wird es entwertet. Wird es später erneut vorgelegt, gilt das als Diebstahl oder Fehlfunktion: die betroffene Sitzung wird komplett gesperrt und der Vorfall protokolliert.&lt;br /&gt;
* &#039;&#039;&#039;Alle Geräte abmelden.&#039;&#039;&#039; Ein gewöhnlicher Endpunkt kann über den Aufruf &#039;&#039;oWriter.JwtRevoke(&#039;all&#039;)&#039;&#039; auch alle Sitzungen eines Benutzers sperren - der Support-Fall „Gerät verloren&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Mit der Einführung dieser Prüfung verlieren &#039;&#039;&#039;alle vorher&lt;br /&gt;
ausgestellten Token&#039;&#039;&#039; ihre Gültigkeit, weil sie zu keiner Sitzung gehören.&lt;br /&gt;
Angemeldete Konsumenten müssen sich nach dem Update einmalig neu anmelden.}}&lt;br /&gt;
&lt;br /&gt;
Der Speicher pflegt sich selbst: Zeilen werden gelöscht, sobald sowohl das&lt;br /&gt;
Access- als auch das Refresh-Token abgelaufen sind. Details und die Skript-Seite:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Fehlerformat==&lt;br /&gt;
&lt;br /&gt;
Jede Fehlerantwort des Servers hat dieselbe Form: ein einziges &#039;&#039;error&#039;&#039;-Objekt&lt;br /&gt;
mit maschinenlesbarem Code, Support-Kennung, Meldung und Korrelations-ID:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;error&amp;quot;: {&lt;br /&gt;
    &amp;quot;code&amp;quot;:    &amp;quot;AUTH_EXPIRED&amp;quot;,&lt;br /&gt;
    &amp;quot;uid&amp;quot;:     &amp;quot;Y00E2RTPDY&amp;quot;,&lt;br /&gt;
    &amp;quot;message&amp;quot;: &amp;quot;Ungültiges oder abgelaufenes Token&amp;quot;,&lt;br /&gt;
    &amp;quot;traceId&amp;quot;: &amp;quot;20260817T091233123-00001A&amp;quot;&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;code&#039;&#039;&#039; - sprechender Bezeichner (z.B. &#039;&#039;AUTH_EXPIRED&#039;&#039;, &#039;&#039;NOT_FOUND&#039;&#039;, &#039;&#039;RATE_LIMITED&#039;&#039;, &#039;&#039;INTERNAL_ERROR&#039;&#039;), an dem ein Client seine Reaktion festmachen kann, ohne Texte auszuwerten.&lt;br /&gt;
* &#039;&#039;&#039;uid&#039;&#039;&#039; - OBS-Kennung der Codestelle. Für die Support-Recherche gedacht, nicht zur Auswertung im Client.&lt;br /&gt;
* &#039;&#039;&#039;message&#039;&#039;&#039; - Text für den Client. Interne Fehlerdetails stehen nie darin, sondern ausschliesslich im Protokoll &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;traceId&#039;&#039;&#039; - Korrelations-ID. Steht zusätzlich im Header &#039;&#039;X-Trace-Id&#039;&#039; und in jeder zugehörigen Logzeile.&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt-Skript kann eigene Codes setzen; siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;Fehlercode&#039;&#039;, &#039;&#039;Nachricht&#039;&#039; und &#039;&#039;traceId&#039;&#039; stehen &#039;&#039;&#039;nur&#039;&#039;&#039; im&lt;br /&gt;
&#039;&#039;error&#039;&#039;-Objekt, nicht flach daneben - der Wert von&lt;br /&gt;
&#039;&#039;Fehlercode&#039;&#039; heisst jetzt &#039;&#039;error.uid&#039;&#039;, &#039;&#039;Nachricht&#039;&#039; entspricht&lt;br /&gt;
&#039;&#039;error.message&#039;&#039;. Clients, die die flachen Felder auswerten, müssen angepasst&lt;br /&gt;
werden.}}&lt;br /&gt;
&lt;br /&gt;
==Sicherheitsmerkmale==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server enthält eine Reihe von Schutzmechanismen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Rate-Limit:&#039;&#039;&#039; Standardmässig max. 10 fehlgeschlagene und 120 erfolgreiche Anfragen pro API-Key (Account) bzw. IP und 60 Sekunden, max. 600 Anfragen global pro 60 Sekunden. Beim Überschreiten wird HTTP 429 mit &#039;&#039;Retry-After&#039;&#039;-Header zurückgegeben. Die vier Schwellen lassen sich &#039;&#039;&#039;pro Server-Profil&#039;&#039;&#039; hinterlegen; die Zähler laufen ebenfalls getrennt je Profil, ein überlastetes Profil bremst das andere also nicht mit aus. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
* &#039;&#039;&#039;Body-Limit:&#039;&#039;&#039; max. 10 MB Request-Body (JSON), max. 1024 Zeichen pro Header- oder Query-Parameter. Datei-Uploads laufen über einen separaten Pfad und sind pro Endpunkt auf &amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt; MB begrenzt (Standard 25&amp;amp;nbsp;MB).&lt;br /&gt;
* &#039;&#039;&#039;Sensible Felder&#039;&#039;&#039; (&#039;&#039;password&#039;&#039;, &#039;&#039;token&#039;&#039;, &#039;&#039;secret&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, ...) werden in Protokoll-Ausgaben automatisch maskiert.&lt;br /&gt;
* &#039;&#039;&#039;Widerrufbare Token:&#039;&#039;&#039; Jedes ausgestellte Token-Paar gehört zu einer Sitzung in &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; und wird bei &#039;&#039;&#039;jedem&#039;&#039;&#039; Zugriff dagegen geprüft. Eine Abmeldung wirkt damit sofort und nicht erst mit dem Ablauf des Tokens; ein Refresh-Token gilt genau einmal. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
* &#039;&#039;&#039;Reserviertes Präfix:&#039;&#039;&#039; Parameter-Namen mit Präfix &#039;&#039;_OBS_&#039;&#039; können von aussen nicht gesetzt werden, sie sind für den Server reserviert (z.B. JWT-Claims).&lt;br /&gt;
* &#039;&#039;&#039;Geblockte Header:&#039;&#039;&#039; &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, &#039;&#039;proxy-authorization&#039;&#039;, &#039;&#039;x-forwarded-for&#039;&#039;, &#039;&#039;x-real-ip&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;api_key&#039;&#039; werden nicht an das Skript durchgereicht.&lt;br /&gt;
* &#039;&#039;&#039;TLS:&#039;&#039;&#039; Bei Profilen mit aktivem TLS fährt der Server ohne Zertifikat und Key nicht hoch. Plain-HTTP ist nur für ein dediziertes Debug-Profil möglich.&lt;br /&gt;
* &#039;&#039;&#039;HSTS:&#039;&#039;&#039; Antworten eines TLS-Profils tragen &#039;&#039;Strict-Transport-Security: max-age=31536000; includeSubDomains&#039;&#039;. Ein Browser spricht die Adresse danach ein Jahr lang nur noch über HTTPS an. Beim Debug-Profil &#039;&#039;Kein SSL&#039;&#039; wird der Header bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; gesetzt - der Browser würde ihn sonst speichern und die Erzwingung auch auf produktive Sitzungen anwenden.&lt;br /&gt;
* &#039;&#039;&#039;mTLS&#039;&#039;&#039; (Mutual TLS): erzwingt, dass jeder Client ein gültiges Client-Zertifikat vorlegt. Optional kann pro Zugang ein erwarteter Subject-DN hinterlegt werden.&lt;br /&gt;
* &#039;&#039;&#039;DNS-Cache:&#039;&#039;&#039; Host-zu-IP-Auflösungen für die Zugangs-Host-Prüfung werden 5 Minuten gecached.&lt;br /&gt;
* &#039;&#039;&#039;Automatische Zertifikatserneuerung:&#039;&#039;&#039; Ein TLS-Profil kann sein Zertifikat selbst bei einer Zertifizierungsstelle beantragen und vor Ablauf erneuern. Der Dienst prüft wöchentlich, ob der Server dafür von aussen erreichbar ist, und tauscht das Zertifikat anschliessend ohne Neustart des Dienstes - nur das betroffene Profil startet neu. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
&lt;br /&gt;
==Protokoll==&lt;br /&gt;
&lt;br /&gt;
Die Tabelle &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; enthält alle relevanten Ereignisse: erfolgreiche und abgelehnte Anfragen, TLS-/JWT-Fehler, Bindungs-Ereignisse, Skript-Fehler. Einträge sind nach Datum, Uhrzeit und Host (IP oder Account-Name) sortiert. Das Protokoll ist die erste Anlaufstelle bei Störungen. Jede Antwort trägt zudem eine Korrelations-ID (Response-Header &#039;&#039;X-Trace-Id&#039;&#039;), die in den Protokoll-Einträgen wiederzufinden ist.&lt;br /&gt;
&lt;br /&gt;
Läuft der REST-Server hinter einem Reverse-Proxy, steht als Host die IP-Adresse des Proxys. Schickt der Proxy den Header &#039;&#039;X-Forwarded-For&#039;&#039; mit, hängt der Server dessen Wert in Protokoll und Konsole als &amp;lt;code&amp;gt;[xff=…]&amp;lt;/code&amp;gt; an den Eintrag an. Der Wert ist eine Angabe des Absenders und wird nur protokolliert. Host-Beschränkung und Rate-Limit verwenden immer die IP-Adresse der Verbindung. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung#Betrieb hinter einem Reverse-Proxy|Betrieb hinter einem Reverse-Proxy]].&lt;br /&gt;
&lt;br /&gt;
==Statistik==&lt;br /&gt;
&lt;br /&gt;
Die Tabelle &#039;&#039;&#039;RESTSRV_STATS&#039;&#039;&#039; enthält eine Statistik aller bearbeiteten Anfragen&lt;br /&gt;
mit IP, Zugang, Endpunkt, Methode, angefragtem Pfad, HTTP-Status und Laufzeit in&lt;br /&gt;
Millisekunden. Dazu kommen zwei Felder, die den Sprung ins Protokoll erlauben:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;rs_trace&#039;&#039;&#039; - dieselbe Korrelations-ID, die auch im Header &#039;&#039;X-Trace-Id&#039;&#039; und in jeder zugehörigen Protokollzeile steht. Damit findet man zu einer auffälligen Statistikzeile alle Protokolleinträge derselben Anfrage.&lt;br /&gt;
* &#039;&#039;&#039;rs_server&#039;&#039;&#039; - das Server-Profil, über das die Anfrage hereinkam. Bei mehreren Profilen (Public-TLS neben Internal-mTLS) ist das die einzige Angabe, an der sich der Weg ablesen lässt.&lt;br /&gt;
&lt;br /&gt;
Weil der Normalfall keine Protokollzeile mehr schreibt, ist RESTSRV_STATS die&lt;br /&gt;
vollständige Sicht auf den Betrieb - RESTSRV_PROTO die auf die Auffälligkeiten.&lt;br /&gt;
&lt;br /&gt;
Aufruf der Statistik:&lt;br /&gt;
&lt;br /&gt;
* Aus der Endpunkt-Liste: F8 zeigt die Statistik des markierten Endpunkts.&lt;br /&gt;
* Aus der Zugänge-Liste: F8 zeigt die Statistik des markierten Zugangs.&lt;br /&gt;
&lt;br /&gt;
==Konsole==&lt;br /&gt;
&lt;br /&gt;
Wird der REST-Server nicht als Dienst, sondern interaktiv gestartet, erscheint eine&lt;br /&gt;
farbige Konsole mit Live-Log und einem &#039;&#039;&#039;fixierten Kopfbereich&#039;&#039;&#039;, der die gerade&lt;br /&gt;
laufenden Anfragen zeigt und sich fortlaufend aktualisiert.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;show &amp;lt;handle&amp;gt;&#039;&#039; || Details einer Anfrage: Request-Header, Protokollzeilen, Antwort. JSON wird farbig formatiert. &#039;&#039;&amp;lt;handle&amp;gt;&#039;&#039; ist die Kennung aus der Abschlusszeile der Anfrage (z.B. &#039;&#039;7F&#039;&#039;), &#039;&#039;&#039;keine&#039;&#039;&#039; laufende Nummer&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;1&#039;&#039; … &#039;&#039;9&#039;&#039; || Auswahl im Kopfbereich - kürzer als &#039;&#039;show&#039;&#039; für die gerade sichtbaren Anfragen. Mit &#039;&#039;mouse on&#039;&#039; auch per Klick&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;ps&#039;&#039; || Listet die gerade laufenden Anfragen auf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;level &amp;lt;stufe&amp;gt;&#039;&#039; || Anzeigestufe des Live-Logs: &#039;&#039;quiet&#039;&#039; , &#039;&#039;normal&#039;&#039; oder &#039;&#039;debug&#039;&#039;. Ohne Parameter wird die aktuelle Stufe genannt&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;filter &amp;lt;ausdruck&amp;gt;&#039;&#039; || Blendet aus, was nicht passt: &#039;&#039;path=&amp;lt;text&amp;gt;&#039;&#039;, &#039;&#039;account=&amp;lt;name&amp;gt;&#039;&#039;, &#039;&#039;status&amp;gt;=&amp;lt;code&amp;gt;&#039;&#039;. &#039;&#039;filter aus&#039;&#039; hebt die Einschränkung auf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;panel on&amp;amp;#124;off&#039;&#039; || Fixierten Kopfbereich ein-/ausschalten. &#039;&#039;&#039;Ausgeschaltet&#039;&#039;&#039; lässt sich der Rückblick-Puffer des Konsolenfensters wieder scrollen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;mouse on&amp;amp;#124;off&#039;&#039; || Klick-Auswahl im Kopfbereich. &#039;&#039;&#039;Eingeschaltet&#039;&#039;&#039; ist das Markieren von Text mit der Maus nicht mehr möglich&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;help&#039;&#039; / &#039;&#039;?&#039;&#039; || Zeigt diese Liste in der Konsole&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;exit&#039;&#039; / &#039;&#039;quit&#039;&#039; || Beendet den Server geordnet&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Anzeigefilter wirkt &#039;&#039;&#039;nur auf die Anzeige&#039;&#039;&#039;. Was die Konsole&lt;br /&gt;
ausblendet, steht vollständig in RESTSRV_PROTO - es geht nichts verloren.}}&lt;br /&gt;
&lt;br /&gt;
Im Service-Modus (Windows-Dienst) ist die Konsole nicht sichtbar. Alle Einträge landen dort ausschliesslich in der Tabelle RESTSRV_PROTO.&lt;br /&gt;
&lt;br /&gt;
==Weiterführende Seiten==&lt;br /&gt;
&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]] - Schritt-für-Schritt-Anleitung&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]] - TLS, mTLS und Bindungen&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]] - API-Keys, JWT, CORS, Host-Beschränkung&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]] - Verwaltung, Berechtigungen, WebHooks&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]] - Aufbau der Endpunkt-Skripte&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]] - Übertragungsprotokoll für Dateien (Multipart, resumable)&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]] - Dateien ausliefern, Range/Resume, öffentliche Artefakte&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel1|Beispiel 1: Daten-Abruf mit JWT]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel2|Beispiel 2: Zeiterfassung als komplette Webanwendung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel3|Beispiel 3: Datensatz anlegen mit JSON-Body (CRUD)]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4: Pfad-Parameter im Routing]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel5|Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6: Datei-Upload und Ablage im DMS]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer&amp;diff=66994</id>
		<title>OBS/Kostenpflichtige Module/RESTServer</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer&amp;diff=66994"/>
		<updated>2026-10-05T06:34:37Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Sicherheitsmerkmale um die automatische Zertifikatserneuerung ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
=REST-Server=&lt;br /&gt;
{{Hinweis|Es handelt sich um ein kostenpflichtiges Modul. Die Schnittstelle muss über den OBS-Support aktiviert werden.}}&lt;br /&gt;
&lt;br /&gt;
==Was leistet der REST-Server?==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server ist die &#039;&#039;&#039;universelle Schnittstelle&#039;&#039;&#039; Ihrer OBS-Installation nach außen. Er macht aus Ihrem OBS einen Dienst, mit dem andere Programme - Webseiten, Apps, Geräte, Kundensysteme, Cloud-Dienste - direkt sprechen können. Statt Daten manuell zu exportieren, zu mailen oder über Umwege bereitzustellen, holen sich angebundene Systeme genau die Information, die sie brauchen, in dem Moment, in dem sie sie brauchen - oder liefern neue Daten direkt in OBS ab.&lt;br /&gt;
&lt;br /&gt;
Für Anwender, die nicht selbst programmieren, bedeutet das: &#039;&#039;&#039;Was bisher nur manuell, per Datei-Import oder über Spezialschnittstellen ging, lässt sich jetzt automatisieren.&#039;&#039;&#039; Eine Webseite zeigt live aktuelle Lagerbestände. Mitarbeiter erfassen Zeiten über das Handy, ohne dass jemand Excel-Listen einpflegen muss. Ein Kunde stößt per Bestelltaste in seinem System einen Vorgang in Ihrem OBS an. Ein Lieferant meldet Wareneingänge automatisch zurück. Jeder dieser Anwendungsfälle wird einmal eingerichtet und läuft danach automatisch.&lt;br /&gt;
&lt;br /&gt;
Technisch gesehen ist der REST-Server ein in OBS integrierter, voll konfigurierbarer &#039;&#039;&#039;HTTP/HTTPS-Dienst&#039;&#039;&#039;, dessen Endpunkte über in OBS gepflegte Pascal-Skripte realisiert werden. Jeder Endpunkt bekommt eine eigene Adresse, eine eigene Logik und eine eigene Berechtigungsstruktur. Rückgaben erfolgen ausschliesslich im JSON-Format. Damit lassen sich beliebige Lese- und Schreibvorgänge auf der OBS-Datenbank realisieren, ohne dass externe Systeme direkten Datenbank-Zugriff erhalten - das ganze OBS-Regelwerk (Rechte, Validierung, Geschäftslogik) bleibt aktiv.&lt;br /&gt;
&lt;br /&gt;
Im Ergebnis ist der REST-Server kein einzelnes Feature, sondern ein &#039;&#039;&#039;Werkzeugkasten&#039;&#039;&#039;: Was an Daten oder Funktionen in OBS verfügbar ist, kann über den REST-Server auch nach außen angeboten - oder von außen entgegengenommen - werden. Die Bandbreite reicht vom einfachen Lese-Endpunkt für ein Webseiten-Widget bis hin zu kompletten B2B-Integrationen mit Mandanten- und Rollen-Trennung.&lt;br /&gt;
&lt;br /&gt;
Aufruf des Moduls in OBS:&lt;br /&gt;
&#039;&#039;&#039;Stammdaten&#039;&#039;&#039; -&amp;gt; &#039;&#039;&#039;Z Weitere Stammdaten&#039;&#039;&#039; -&amp;gt; &#039;&#039;&#039;REST-Server&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Anwendungsbereiche==&lt;br /&gt;
&lt;br /&gt;
Die folgenden Einsatzfälle sind typische Beispiele, an denen sich der praktische Nutzen ablesen lässt. Es ist keine abschliessende Liste - alles, was sich in OBS-Skripten abbilden lässt, kann auch über einen Endpunkt bereitgestellt werden.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Dies sind Beispiele. Für die konkrete Entwicklung können, je nach Komplexität, weitere Kosten anfallen.}}&lt;br /&gt;
&lt;br /&gt;
===Webseiten und Kundenportale===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Live-Daten auf der eigenen Webseite:&#039;&#039;&#039; Lagerbestände, Verfügbarkeiten, Preise, Auftragsstatus aus OBS direkt anzeigen, ohne tägliche Datei-Exports.&lt;br /&gt;
* &#039;&#039;&#039;Kundenportale:&#039;&#039;&#039; Kunden sehen ihre offenen Posten, Auftragshistorie, Lieferscheine. Die Seite holt sich die Daten zur Anzeigezeit direkt aus OBS, jeder Kunde sieht nur seine eigenen Daten.&lt;br /&gt;
* &#039;&#039;&#039;Trackingseiten:&#039;&#039;&#039; Status einer Bestellung, eines Auftrags oder einer Reparatur für Endkunden, abrufbar über eine Sendungsnummer.&lt;br /&gt;
&lt;br /&gt;
===Mobile Anwendungen und Außendienst===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Mobile Zeiterfassung:&#039;&#039;&#039; Mitarbeiter buchen Anfang, Pause und Ende über Smartphone oder Tablet, die Daten landen direkt in OBS - ohne Zettelwirtschaft.&lt;br /&gt;
* &#039;&#039;&#039;QR-/Barcode-Scanner und Lagergeräte:&#039;&#039;&#039; Wareneingang, Inventur, Kommissionierung direkt in OBS abbilden, ohne zwischengeschaltete Software.&lt;br /&gt;
* &#039;&#039;&#039;Außendienst-Apps:&#039;&#039;&#039; Techniker sehen ihre Aufträge, dokumentieren Einsätze, erfassen Material - synchronisiert mit OBS.&lt;br /&gt;
&lt;br /&gt;
===Anbindung von Drittsystemen===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Webshops:&#039;&#039;&#039; Bestellungen werden direkt vom Shop in OBS gemeldet, Lagerbestände und Preise vom Shop bei OBS abgefragt.&lt;br /&gt;
* &#039;&#039;&#039;Buchhaltungs- und ERP-Systeme:&#039;&#039;&#039; Bidirektionaler Austausch von Belegen, Stammdaten und Buchungen.&lt;br /&gt;
* &#039;&#039;&#039;CRM- und Marketing-Tools:&#039;&#039;&#039; Personendaten, Aktivitäten oder Mailing-Empfänger werden synchron gehalten.&lt;br /&gt;
* &#039;&#039;&#039;Logistikdienstleister:&#039;&#039;&#039; Versandaufträge werden übergeben, Tracking-Informationen zurückgeliefert.&lt;br /&gt;
&lt;br /&gt;
===Automatisierte Rückmeldungen (WebHooks)===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Zahlungs-Callbacks:&#039;&#039;&#039; Bezahldienste (z.B. PayPal, Klarna, Stripe) melden den Zahlungseingang an einen WebHook-Endpunkt - OBS bucht automatisch.&lt;br /&gt;
* &#039;&#039;&#039;Versanddienste:&#039;&#039;&#039; Statusmeldungen (verschickt, zugestellt, retour) werden direkt im passenden Vorgang dokumentiert.&lt;br /&gt;
* &#039;&#039;&#039;Cloud-Workflows:&#039;&#039;&#039; Externe Automatisierungs-Plattformen (Make, Zapier, n8n, ...) stoßen OBS-Vorgänge an oder werden von OBS aus angesprochen.&lt;br /&gt;
&lt;br /&gt;
===Geschäftspartner-Kommunikation (B2B)===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lieferanten-Schnittstelle:&#039;&#039;&#039; Bestellungen werden automatisch übergeben, Auftragsbestätigungen und Lieferavise zurückgespielt.&lt;br /&gt;
* &#039;&#039;&#039;Kunden-Schnittstelle:&#039;&#039;&#039; Grosskunden bestellen direkt aus ihrem ERP heraus, Stammdaten und Konditionen werden zentral gepflegt.&lt;br /&gt;
* &#039;&#039;&#039;Sichere Maschine-zu-Maschine-Kommunikation:&#039;&#039;&#039; Mit Mutual-TLS (mTLS) wird sichergestellt, dass nur die freigeschalteten Partnersysteme - und niemand sonst - mit OBS sprechen können.&lt;br /&gt;
&lt;br /&gt;
===Interne Microservices und Automatisierungen===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Eigene kleine Hilfs-Tools:&#039;&#039;&#039; Zeitschaltautomatik, Reporting-Generator, Sammelaktionen, die aus mehreren Quellen Daten in OBS verarbeiten.&lt;br /&gt;
* &#039;&#039;&#039;Verbindung mehrerer Standorte:&#039;&#039;&#039; Verteilte Systeme tauschen Daten über definierte Endpunkte aus, ohne offene Datenbankverbindungen.&lt;br /&gt;
* &#039;&#039;&#039;Datenbereitstellung für Dashboards und Auswertungen:&#039;&#039;&#039; BI-Tools (Power BI, Grafana, ...) lesen aufbereitete Kennzahlen direkt aus OBS.&lt;br /&gt;
&lt;br /&gt;
===Was bedeutet das wirtschaftlich?===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Manuelle Arbeit fällt weg.&#039;&#039;&#039; Daten werden nicht mehr aus OBS exportiert, per Mail geschickt und woanders eingelesen - sie fliessen direkt.&lt;br /&gt;
* &#039;&#039;&#039;Fehler werden weniger.&#039;&#039;&#039; Jede manuelle Stelle ist eine mögliche Fehlerquelle; jede Automatisierung eine Stelle, an der nichts mehr schiefgehen kann.&lt;br /&gt;
* &#039;&#039;&#039;Neue Geschäftsmodelle werden möglich.&#039;&#039;&#039; Kunden- und Lieferantenportale, Self-Service-Funktionen, App-Anbindungen lassen sich aufbauen, ohne ein zweites System pflegen zu müssen.&lt;br /&gt;
* &#039;&#039;&#039;Bestehende Software bleibt verbunden.&#039;&#039;&#039; Statt eine vorhandene Anwendung ablösen zu müssen, kann sie über den REST-Server an OBS angedockt werden - in beiden Richtungen.&lt;br /&gt;
* &#039;&#039;&#039;Skaliert mit:&#039;&#039;&#039; Was klein anfängt (ein einzelner Endpunkt für eine Webseite) kann zu einer kompletten Integrations-Plattform ausgebaut werden, ohne dass die Grundlage gewechselt werden muss.&lt;br /&gt;
&lt;br /&gt;
==Architektur im Überblick==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server besteht aus mehreren aufeinander aufbauenden Bausteinen, die alle in OBS gepflegt werden:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Baustein     !! Tabelle              !! Zweck&lt;br /&gt;
|-&lt;br /&gt;
| Server       || RESTSRV_SERVER       || TLS-Profil + eigene HTTP-Server-Instanz, kann mehrfach existieren&lt;br /&gt;
|-&lt;br /&gt;
| Bindung      || RESTSRV_BINDINGS     || IP-Adresse + Port pro Server&lt;br /&gt;
|-&lt;br /&gt;
| Zugang       || RESTSRV_ACCOUNT      || API-Key, optionale Host- und CORS-Beschränkung, optionale JWT-Konfiguration&lt;br /&gt;
|-&lt;br /&gt;
| Endpunkt     || RESTSRV_ENDPOINTS    || Skript, das die Anfrage bearbeitet, einem Server fest zugeordnet&lt;br /&gt;
|-&lt;br /&gt;
| Berechtigung || RESTSRV_ACCESS       || Verbindet Zugang und Endpunkt&lt;br /&gt;
|-&lt;br /&gt;
| Idempotenz   || RESTSRV_IDEMPOTENCY  || Speicher für Idempotency-Keys und die dazu gelieferte Antwort&lt;br /&gt;
|-&lt;br /&gt;
| Sitzung      || RESTSRV_TOKEN        || Ausgestellte Token-Paare je Sitzung; Grundlage für Rotation und Widerruf&lt;br /&gt;
|-&lt;br /&gt;
| Protokoll    || RESTSRV_PROTO        || Laufende Ereignisse, Fehler, Audit-Einträge&lt;br /&gt;
|-&lt;br /&gt;
| Statistik    || RESTSRV_STATS        || Aufrufzähler, Antwortzeiten und HTTP-Status pro Endpunkt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Eine Anfrage durchläuft folgende Stationen:&lt;br /&gt;
&lt;br /&gt;
# Rate-Limit-Prüfung (pro API-Key bzw. IP und global, Schwellen je Server-Profil)&lt;br /&gt;
# Andrangsgrenze des Server-Profils (&#039;&#039;Max. parallel&#039;&#039;) - bei Überschreitung &#039;&#039;&#039;503&#039;&#039;&#039; mit &#039;&#039;Retry-After&#039;&#039;&lt;br /&gt;
# Authentifizierung über den API-Key (Header &#039;&#039;apikey&#039;&#039;)&lt;br /&gt;
# CORS-Prüfung (sofern &#039;&#039;Origin&#039;&#039;-Header gesetzt)&lt;br /&gt;
# Optional: JWT-Prüfung - Signatur, Ablauf und Sitzung (sofern für den Zugang aktiviert)&lt;br /&gt;
# Routing: Auflösung des Pfads über die Pfad-Templates der Endpunkte&lt;br /&gt;
# Prüfung der Berechtigung (Zugang -&amp;gt; Endpunkt)&lt;br /&gt;
# Entpacken des Anfragekörpers, sofern er mit &#039;&#039;Content-Encoding: gzip&#039;&#039; gesendet wurde&lt;br /&gt;
# Idempotenz-Prüfung bei POST/PUT/PATCH/DELETE - der Header &#039;&#039;Idempotency-Key&#039;&#039; ist dort &#039;&#039;&#039;Pflicht&#039;&#039;&#039;&lt;br /&gt;
# Ausführung des Endpunkt-Skripts (oder WebHook-Antwort)&lt;br /&gt;
# Festschreiben des Ergebnisses im Idempotenz-Speicher (sofern in Schritt 9 reserviert)&lt;br /&gt;
# JSON-Antwort und Statistik-Eintrag&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Die Reihenfolge ist kein Zufall.&#039;&#039;&#039; Das Entpacken liegt &#039;&#039;&#039;hinter&#039;&#039;&#039;&lt;br /&gt;
der Authentifizierung, damit ein Unbefugter dem Server keine Rechenzeit für eine&lt;br /&gt;
Dekompressionsbombe abverlangen kann - und &#039;&#039;&#039;vor&#039;&#039;&#039; der Idempotenz-Reservierung,&lt;br /&gt;
damit eine abgewiesene Anfrage keinen &#039;&#039;Idempotency-Key&#039;&#039; verbrennt.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Der Normalfall erzeugt keine Protokollzeile.&#039;&#039;&#039; Bis 08/2026 schrieb jede&lt;br /&gt;
Anfrage eine Zeile nach RESTSRV_PROTO; sie ist entfallen, weil RESTSRV_STATS&lt;br /&gt;
zu derselben Anfrage IP, Zugang, Endpunkt, Methode, Pfad, Status, Laufzeit und&lt;br /&gt;
dieselbe Korrelations-ID trägt - und länger aufbewahrt wird. Das Protokoll trägt&lt;br /&gt;
seitdem nur noch Auffälligkeiten.}}&lt;br /&gt;
&lt;br /&gt;
==Adressierung==&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt wird über ein &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; angesprochen, das den Pfad nach dem Host beschreibt und Platzhalter enthalten kann:&lt;br /&gt;
&lt;br /&gt;
 http://[Hostadresse][:Port][Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Beispiele:&lt;br /&gt;
&lt;br /&gt;
 https://api.meinserver.de/kalender/v1&lt;br /&gt;
 https://api.meinserver.de/orders/{uid}&lt;br /&gt;
 https://api.meinserver.de/orders/{uid}/modules/{code}&lt;br /&gt;
&lt;br /&gt;
Statische Segmente müssen exakt passen; Platzhalter in geschweiften Klammern (z.B. {uid}) matchen einen beliebigen Wert und stehen im Skript als Pfad-Parameter zur Verfügung. Eine Versionskennung wird als statisches Segment ins Template aufgenommen (z.B. /kalender/v1), um eine Funktionalität zu verändern, ohne bestehende Konsumenten zu brechen. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].&lt;br /&gt;
&lt;br /&gt;
==Unterstützte HTTP-Methoden==&lt;br /&gt;
&lt;br /&gt;
Der Server akzeptiert die Methoden &#039;&#039;&#039;GET&#039;&#039;&#039;, &#039;&#039;&#039;POST&#039;&#039;&#039;, &#039;&#039;&#039;PUT&#039;&#039;&#039;, &#039;&#039;&#039;DELETE&#039;&#039;&#039; und &#039;&#039;&#039;PATCH&#039;&#039;&#039; sowie &#039;&#039;&#039;OPTIONS&#039;&#039;&#039; (CORS-Preflight, ohne Skript-Aufruf). Jede Methode wird im Endpunkt-Skript als gleichnamige Prozedur implementiert - &#039;&#039;&#039;mit einer Ausnahme&#039;&#039;&#039;: &#039;&#039;Delete&#039;&#039; ist in der Skriptsprache ein reservierter Name und lässt sich nicht als Prozedur deklarieren. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
Fehlt die zur Methode passende Prozedur, antwortet der Server mit &#039;&#039;&#039;405&#039;&#039;&#039; und&lt;br /&gt;
nennt im Header &#039;&#039;Allow&#039;&#039; die Verben, die dieses Skript tatsächlich anbietet.&lt;br /&gt;
&lt;br /&gt;
==Kompression==&lt;br /&gt;
&lt;br /&gt;
Der Server packt Antworten mit &#039;&#039;&#039;gzip&#039;&#039;&#039;, wenn der Client sie annimmt. Ausgehandelt wird über den Anfrage-Header &amp;lt;code&amp;gt;Accept-Encoding&amp;lt;/code&amp;gt;; die Antwort trägt dann &amp;lt;code&amp;gt;Content-Encoding: gzip&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;Vary: Accept-Encoding&amp;lt;/code&amp;gt; - bei einer CORS-Anfrage steht dort &amp;lt;code&amp;gt;Vary: Origin, Accept-Encoding&amp;lt;/code&amp;gt;, also &#039;&#039;&#039;eine&#039;&#039;&#039; Zeile mit beiden Bedingungen. Browser, &amp;lt;code&amp;gt;curl --compressed&amp;lt;/code&amp;gt; und die gängigen HTTP-Bibliotheken entpacken selbstständig - für den Konsumenten ändert sich nichts ausser der übertragenen Datenmenge.&lt;br /&gt;
&lt;br /&gt;
Gepackt wird nur, wo es etwas bringt:&lt;br /&gt;
&lt;br /&gt;
* Nur JSON (auch &#039;&#039;problem+json&#039;&#039; und &#039;&#039;ndjson&#039;&#039;), XML, JavaScript, SVG und alles unter &#039;&#039;text/&#039;&#039; - darunter Text, CSV und HTML. Bereits komprimierte Typen - PDF, Bild, ZIP, Video - werden durch gzip &#039;&#039;&#039;grösser&#039;&#039;&#039;, nicht kleiner.&lt;br /&gt;
* Erst ab &#039;&#039;&#039;1024 Byte&#039;&#039;&#039;. Darunter kostet der gzip-Rahmen mehr, als der Inhalt einspart.&lt;br /&gt;
* &#039;&#039;&#039;Nicht&#039;&#039;&#039; bei Dateiantworten, nicht bei Teilauslieferungen (206) und nicht bei Antworten ohne Körper (204, 304).&lt;br /&gt;
* Nicht, wenn das Endpunkt-Skript den Header &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; selbst gesetzt hat - dann gehört die Antwort dem Skript.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;deflate&#039;&#039;&#039; wird in &#039;&#039;&#039;keiner&#039;&#039;&#039; Richtung unterstützt. Der Wert ist historisch uneindeutig - die einen senden RFC-1950-gerahmt, die anderen roh -, und wer ihn nennt, beherrscht ohnehin gzip. Eine Anfrage mit &amp;lt;code&amp;gt;Content-Encoding: deflate&amp;lt;/code&amp;gt; wird mit 415 abgewiesen.&lt;br /&gt;
&lt;br /&gt;
Abschalten lässt sich das Packen je Server-Profil über den Haken &#039;&#039;Keine Kompression&#039;&#039; (siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]). Der Haken betrifft ausschliesslich die Antwortrichtung.&lt;br /&gt;
&lt;br /&gt;
===Komprimierte Anfragen===&lt;br /&gt;
&lt;br /&gt;
In der Gegenrichtung nimmt der Server &#039;&#039;&#039;gzip&#039;&#039;&#039; entgegen: Trägt eine Anfrage den Header &amp;lt;code&amp;gt;Content-Encoding: gzip&amp;lt;/code&amp;gt;, wird der Körper entpackt, bevor ihn jemand liest. Das Endpunkt-Skript merkt davon nichts.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Anfrage                                              !! Antwort&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; fehlt oder &amp;lt;code&amp;gt;identity&amp;lt;/code&amp;gt; || normale Verarbeitung; ab 10 MB Körpergrösse &#039;&#039;&#039;413&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;gzip&amp;lt;/code&amp;gt;                                    || Körper wird entpackt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;deflate&amp;lt;/code&amp;gt;, anderer Wert (z.B. &amp;lt;code&amp;gt;br&amp;lt;/code&amp;gt;) oder mehrere Werte || &#039;&#039;&#039;415&#039;&#039;&#039; &amp;lt;code&amp;gt;UNSUPPORTED_MEDIA_TYPE&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Gepackter Körper bei einem Datei-Upload              || &#039;&#039;&#039;415&#039;&#039;&#039; - siehe [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]]&lt;br /&gt;
|-&lt;br /&gt;
| Entpackter Körper über &#039;&#039;&#039;10 MB&#039;&#039;&#039;                    || &#039;&#039;&#039;413&#039;&#039;&#039; &amp;lt;code&amp;gt;PAYLOAD_TOO_LARGE&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Entpackter Körper über dem &#039;&#039;&#039;400fachen&#039;&#039;&#039; der gesendeten Grösse || &#039;&#039;&#039;413&#039;&#039;&#039; &amp;lt;code&amp;gt;PAYLOAD_TOO_LARGE&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Für einen gepackten Anfragekörper gelten &#039;&#039;&#039;zwei&#039;&#039;&#039; Grenzen. Beide zählen den &#039;&#039;&#039;entpackten&#039;&#039;&#039; Inhalt, und beide greifen bereits &#039;&#039;&#039;während&#039;&#039;&#039; des Entpackens - ein Körper, der sie reisst, belegt den Speicher gar nicht erst:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;10 MB&#039;&#039;&#039; absolut. Dieselbe Grenze, die auch für einen ungepackten Körper gilt.&lt;br /&gt;
* &#039;&#039;&#039;das 400fache&#039;&#039;&#039; der gesendeten Grösse, &#039;&#039;&#039;mindestens aber 64 KB&#039;&#039;&#039;. Damit wird eine &#039;&#039;Dekompressionsbombe&#039;&#039; - wenige Kilobyte, die sich auf Hunderte Megabyte aufblähen - schon nach wenigen Prozent abgebrochen. Der Boden von 64 KB sorgt dafür, dass die Verhältnisrechnung winzige Körper gar nicht erst trifft: ein 100-Byte-Körper darf sich auf 64 KB entpacken, nicht nur auf 40 KB.&lt;br /&gt;
&lt;br /&gt;
Die zweite Grenze trifft normale Nutzlasten nicht. Gemessen erreichen echte JSON-Körper Verhältnisse von 24:1 bis 40:1, und selbst eine Liste aus zehntausenden identischen Zeilen kommt nur auf rund 340:1 - mehr gibt das Verfahren für wiederholte Datensätze nicht her. Über 400:1 kommt nur, wer sehr lange Läufe desselben Zeichens sendet.&lt;br /&gt;
&lt;br /&gt;
Für Datei-Uploads gilt keine der beiden Grenzen - dort zählt allein die je Endpunkt eingestellte Maximalgrösse.}}&lt;br /&gt;
&lt;br /&gt;
==Persistente Verbindungen (Keep-alive)==&lt;br /&gt;
&lt;br /&gt;
Der Server hält eine Verbindung nach der Antwort &#039;&#039;&#039;offen&#039;&#039;&#039;, statt sie zu&lt;br /&gt;
schliessen. Ein Client, der mehrere Anfragen hintereinander stellt - eine App,&lt;br /&gt;
die eine Liste seitenweise abholt -, spart damit je Folgeanfrage den Aufbau von&lt;br /&gt;
TCP-Verbindung und TLS-Sitzung.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Grenze !! Wert !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| Leerlauf || &#039;&#039;&#039;10 Sekunden&#039;&#039;&#039; || Passiert auf der Verbindung so lange nichts, wird sie geschlossen&lt;br /&gt;
|-&lt;br /&gt;
| Anfragen je Verbindung || &#039;&#039;&#039;100&#039;&#039;&#039; || Danach schliesst der Server geordnet, der Client baut neu auf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beides ist für den Konsumenten unkritisch: Eine geschlossene Verbindung ist kein&lt;br /&gt;
Fehler, jede HTTP-Bibliothek baut sie bei der nächsten Anfrage selbst wieder auf.&lt;br /&gt;
Wer viele Anfragen hintereinander stellt, gewinnt aber deutlich, wenn er seinen&lt;br /&gt;
Client die Verbindung wiederverwenden lässt (&#039;&#039;Connection: keep-alive&#039;&#039; ist der&lt;br /&gt;
Standard von HTTP/1.1, die meisten Bibliotheken tun es von allein).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Die Leerlaufgrenze gilt für jeden Lesevorgang der Verbindung&#039;&#039;&#039;,&lt;br /&gt;
nicht nur für das Warten auf die nächste Anfrage. Ein Client, der seinen&lt;br /&gt;
Anfragekörper nur tropfenweise sendet und dazwischen länger als 10 Sekunden&lt;br /&gt;
pausiert, verliert die Verbindung mitten im Senden.}}&lt;br /&gt;
&lt;br /&gt;
Die Werte sind bewusst knapp gewählt: Der Server fährt einen eigenen Thread je&lt;br /&gt;
Verbindung, eine leerlaufende Verbindung kostet also nicht bloss einen&lt;br /&gt;
Dateizeiger. Sie sind keine Lastgrenze - dafür gibt es &#039;&#039;Max. Verbindungen&#039;&#039; und&lt;br /&gt;
&#039;&#039;Max. parallel&#039;&#039; am Server-Profil (siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]).&lt;br /&gt;
&lt;br /&gt;
==Datei-Uploads==&lt;br /&gt;
&lt;br /&gt;
Endpunkte, die dafür freigeschaltet sind (&amp;lt;code&amp;gt;re_upload&amp;lt;/code&amp;gt;), nehmen Dateien&lt;br /&gt;
per &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; oder als fortsetzbaren &#039;&#039;&#039;Resumable-Upload&#039;&#039;&#039;&lt;br /&gt;
(&amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;) entgegen. Die maximale Grösse wird pro Endpunkt&lt;br /&gt;
festgelegt (&amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;, Standard 25&amp;amp;nbsp;MB); Überschreitung&lt;br /&gt;
ergibt 413, ein Upload an einen nicht freigeschalteten Endpunkt 415. Der Server&lt;br /&gt;
legt die Datei temporär ab und übergibt dem Skript den Pfad; die weitere&lt;br /&gt;
Verarbeitung (z.B. DMS-Ablage) übernimmt das Skript. Details:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
==Doppelte Sendungen abfangen (Idempotenz)==&lt;br /&gt;
&lt;br /&gt;
Ein Client, der nach einem Timeout dieselbe Anfrage erneut schickt, darf keine&lt;br /&gt;
Zweitbuchung auslösen. Der Server erledigt das selbst: Jeder Aufruf mit&lt;br /&gt;
&#039;&#039;&#039;POST&#039;&#039;&#039;, &#039;&#039;&#039;PUT&#039;&#039;&#039;, &#039;&#039;&#039;PATCH&#039;&#039;&#039; oder &#039;&#039;&#039;DELETE&#039;&#039;&#039; muss den Header&lt;br /&gt;
&amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; tragen; der Server merkt sich Schlüssel und Ergebnis&lt;br /&gt;
in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;. Eine Wiederholung mit demselben Schlüssel und&lt;br /&gt;
demselben Inhalt bekommt die &#039;&#039;&#039;gespeicherte Antwort&#039;&#039;&#039; zurück - das&lt;br /&gt;
Endpunkt-Skript läuft gar nicht erst an.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Der Header ist Pflicht, nicht optional.&#039;&#039;&#039; Ein schreibender Aufruf&lt;br /&gt;
&#039;&#039;&#039;ohne&#039;&#039;&#039; &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; wird mit &#039;&#039;&#039;400&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt; abgewiesen, und das Endpunkt-Skript läuft&lt;br /&gt;
nicht an.&lt;br /&gt;
&lt;br /&gt;
Das war bis v1.21 anders: fehlte der Header, lief die Anfrage ungeschützt durch.&lt;br /&gt;
Der Schutz gegen eine Doppelbuchung stand damit in der Zusage des Clients und&lt;br /&gt;
nicht in einer Prüfung des Servers - ein Wiederholversuch nach Zeitüberschreitung&lt;br /&gt;
buchte ein zweites Mal. Konsumenten, die bisher ohne den Header schreiben, müssen&lt;br /&gt;
angepasst werden.}}&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Pflicht nur für schreibende Verben.&#039;&#039;&#039; &#039;&#039;GET&#039;&#039; braucht keinen Schlüssel und kennt die Mechanik nicht.&lt;br /&gt;
* &#039;&#039;&#039;Der JWT-Endpunkt ist ausgenommen.&#039;&#039;&#039; Anmelden, Erneuern und Abmelden laufen vor der Adressauflösung und brauchen keinen Schlüssel.&lt;br /&gt;
* &#039;&#039;&#039;Kein Schalter am Endpunkt.&#039;&#039;&#039; Die Mechanik greift für jeden Endpunkt automatisch.&lt;br /&gt;
* &#039;&#039;&#039;Das Skript muss nichts tun.&#039;&#039;&#039; Eine eigene Idempotenz-Logik im Skript ist nicht nötig.&lt;br /&gt;
* &#039;&#039;&#039;Pro Vorgang ein Schlüssel&#039;&#039;&#039;, über alle Wiederholversuche hinweg unverändert - nicht bei jedem Versuch neu erzeugt.&lt;br /&gt;
&lt;br /&gt;
Details und die Statuscodes: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]], Abschnitt Idempotenz.&lt;br /&gt;
&lt;br /&gt;
==Sitzungen und Abmelden==&lt;br /&gt;
&lt;br /&gt;
Ein JWT trägt seine Gültigkeit in sich: der Server prüft Signatur und Ablauf und&lt;br /&gt;
braucht dafür keine Datenbank. Genau deshalb liesse sich ein ausgegebener Token&lt;br /&gt;
von sich aus auch nicht mehr zurückziehen - ein verlorenes Gerät wäre bis zum&lt;br /&gt;
Ablauf des Tokens weiter nutzbar.&lt;br /&gt;
&lt;br /&gt;
Der Server führt deshalb zu jedem ausgestellten Token-Paar eine Zeile in&lt;br /&gt;
&#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; und prüft jeden Zugriff dagegen. Daraus ergeben sich drei&lt;br /&gt;
Eigenschaften:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Abmelden wirkt sofort.&#039;&#039;&#039; Ein &#039;&#039;DELETE&#039;&#039; auf den JWT-Endpunkt sperrt die Sitzung; alle Token dieser Sitzung sind damit unmittelbar ungültig, auch die noch nicht abgelaufenen. Anmelden, Erneuern und Abmelden laufen also über einen einzigen Endpunkt, und für das Abmelden ist kein Skript nötig.&lt;br /&gt;
* &#039;&#039;&#039;Ein Refresh-Token gilt genau einmal.&#039;&#039;&#039; Beim Erneuern wird es entwertet. Wird es später erneut vorgelegt, gilt das als Diebstahl oder Fehlfunktion: die betroffene Sitzung wird komplett gesperrt und der Vorfall protokolliert.&lt;br /&gt;
* &#039;&#039;&#039;Alle Geräte abmelden.&#039;&#039;&#039; Ein gewöhnlicher Endpunkt kann über den Aufruf &#039;&#039;oWriter.JwtRevoke(&#039;all&#039;)&#039;&#039; auch alle Sitzungen eines Benutzers sperren - der Support-Fall „Gerät verloren&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Mit der Einführung dieser Prüfung verlieren &#039;&#039;&#039;alle vorher&lt;br /&gt;
ausgestellten Token&#039;&#039;&#039; ihre Gültigkeit, weil sie zu keiner Sitzung gehören.&lt;br /&gt;
Angemeldete Konsumenten müssen sich nach dem Update einmalig neu anmelden.}}&lt;br /&gt;
&lt;br /&gt;
Der Speicher pflegt sich selbst: Zeilen werden gelöscht, sobald sowohl das&lt;br /&gt;
Access- als auch das Refresh-Token abgelaufen sind. Details und die Skript-Seite:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Fehlerformat==&lt;br /&gt;
&lt;br /&gt;
Jede Fehlerantwort des Servers hat dieselbe Form: ein einziges &#039;&#039;error&#039;&#039;-Objekt&lt;br /&gt;
mit maschinenlesbarem Code, Support-Kennung, Meldung und Korrelations-ID:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;error&amp;quot;: {&lt;br /&gt;
    &amp;quot;code&amp;quot;:    &amp;quot;AUTH_EXPIRED&amp;quot;,&lt;br /&gt;
    &amp;quot;uid&amp;quot;:     &amp;quot;Y00E2RTPDY&amp;quot;,&lt;br /&gt;
    &amp;quot;message&amp;quot;: &amp;quot;Ungültiges oder abgelaufenes Token&amp;quot;,&lt;br /&gt;
    &amp;quot;traceId&amp;quot;: &amp;quot;20260817T091233123-00001A&amp;quot;&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;code&#039;&#039;&#039; - sprechender Bezeichner (z.B. &#039;&#039;AUTH_EXPIRED&#039;&#039;, &#039;&#039;NOT_FOUND&#039;&#039;, &#039;&#039;RATE_LIMITED&#039;&#039;, &#039;&#039;INTERNAL_ERROR&#039;&#039;), an dem ein Client seine Reaktion festmachen kann, ohne Texte auszuwerten.&lt;br /&gt;
* &#039;&#039;&#039;uid&#039;&#039;&#039; - OBS-Kennung der Codestelle. Für die Support-Recherche gedacht, nicht zur Auswertung im Client.&lt;br /&gt;
* &#039;&#039;&#039;message&#039;&#039;&#039; - Text für den Client. Interne Fehlerdetails stehen nie darin, sondern ausschliesslich im Protokoll &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;traceId&#039;&#039;&#039; - Korrelations-ID. Steht zusätzlich im Header &#039;&#039;X-Trace-Id&#039;&#039; und in jeder zugehörigen Logzeile.&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt-Skript kann eigene Codes setzen; siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;Fehlercode&#039;&#039;, &#039;&#039;Nachricht&#039;&#039; und &#039;&#039;traceId&#039;&#039; stehen &#039;&#039;&#039;nur&#039;&#039;&#039; im&lt;br /&gt;
&#039;&#039;error&#039;&#039;-Objekt, nicht flach daneben - der Wert von&lt;br /&gt;
&#039;&#039;Fehlercode&#039;&#039; heisst jetzt &#039;&#039;error.uid&#039;&#039;, &#039;&#039;Nachricht&#039;&#039; entspricht&lt;br /&gt;
&#039;&#039;error.message&#039;&#039;. Clients, die die flachen Felder auswerten, müssen angepasst&lt;br /&gt;
werden.}}&lt;br /&gt;
&lt;br /&gt;
==Sicherheitsmerkmale==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server enthält eine Reihe von Schutzmechanismen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Rate-Limit:&#039;&#039;&#039; Standardmässig max. 10 fehlgeschlagene und 120 erfolgreiche Anfragen pro API-Key (Account) bzw. IP und 60 Sekunden, max. 600 Anfragen global pro 60 Sekunden. Beim Überschreiten wird HTTP 429 mit &#039;&#039;Retry-After&#039;&#039;-Header zurückgegeben. Die vier Schwellen lassen sich &#039;&#039;&#039;pro Server-Profil&#039;&#039;&#039; hinterlegen; die Zähler laufen ebenfalls getrennt je Profil, ein überlastetes Profil bremst das andere also nicht mit aus. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
* &#039;&#039;&#039;Body-Limit:&#039;&#039;&#039; max. 10 MB Request-Body (JSON), max. 1024 Zeichen pro Header- oder Query-Parameter. Datei-Uploads laufen über einen separaten Pfad und sind pro Endpunkt auf &amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt; MB begrenzt (Standard 25&amp;amp;nbsp;MB).&lt;br /&gt;
* &#039;&#039;&#039;Sensible Felder&#039;&#039;&#039; (&#039;&#039;password&#039;&#039;, &#039;&#039;token&#039;&#039;, &#039;&#039;secret&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, ...) werden in Protokoll-Ausgaben automatisch maskiert.&lt;br /&gt;
* &#039;&#039;&#039;Widerrufbare Token:&#039;&#039;&#039; Jedes ausgestellte Token-Paar gehört zu einer Sitzung in &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; und wird bei &#039;&#039;&#039;jedem&#039;&#039;&#039; Zugriff dagegen geprüft. Eine Abmeldung wirkt damit sofort und nicht erst mit dem Ablauf des Tokens; ein Refresh-Token gilt genau einmal. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
* &#039;&#039;&#039;Reserviertes Präfix:&#039;&#039;&#039; Parameter-Namen mit Präfix &#039;&#039;_OBS_&#039;&#039; können von aussen nicht gesetzt werden, sie sind für den Server reserviert (z.B. JWT-Claims).&lt;br /&gt;
* &#039;&#039;&#039;Geblockte Header:&#039;&#039;&#039; &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, &#039;&#039;proxy-authorization&#039;&#039;, &#039;&#039;x-forwarded-for&#039;&#039;, &#039;&#039;x-real-ip&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;api_key&#039;&#039; werden nicht an das Skript durchgereicht.&lt;br /&gt;
* &#039;&#039;&#039;TLS:&#039;&#039;&#039; Bei Profilen mit aktivem TLS fährt der Server ohne Zertifikat und Key nicht hoch. Plain-HTTP ist nur für ein dediziertes Debug-Profil möglich.&lt;br /&gt;
* &#039;&#039;&#039;HSTS:&#039;&#039;&#039; Antworten eines TLS-Profils tragen &#039;&#039;Strict-Transport-Security: max-age=31536000; includeSubDomains&#039;&#039;. Ein Browser spricht die Adresse danach ein Jahr lang nur noch über HTTPS an. Beim Debug-Profil &#039;&#039;Kein SSL&#039;&#039; wird der Header bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; gesetzt - der Browser würde ihn sonst speichern und die Erzwingung auch auf produktive Sitzungen anwenden.&lt;br /&gt;
* &#039;&#039;&#039;mTLS&#039;&#039;&#039; (Mutual TLS): erzwingt, dass jeder Client ein gültiges Client-Zertifikat vorlegt. Optional kann pro Zugang ein erwarteter Subject-DN hinterlegt werden.&lt;br /&gt;
* &#039;&#039;&#039;DNS-Cache:&#039;&#039;&#039; Host-zu-IP-Auflösungen für die Zugangs-Host-Prüfung werden 5 Minuten gecached.&lt;br /&gt;
* &#039;&#039;&#039;Automatische Zertifikatserneuerung:&#039;&#039;&#039; Ein TLS-Profil kann sein Zertifikat selbst bei einer Zertifizierungsstelle beantragen und vor Ablauf erneuern. Der Dienst prüft wöchentlich, ob der Server dafür von aussen erreichbar ist, und tauscht das Zertifikat anschliessend ohne Neustart des Dienstes - nur das betroffene Profil startet neu. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
&lt;br /&gt;
==Protokoll==&lt;br /&gt;
&lt;br /&gt;
Die Tabelle &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; enthält alle relevanten Ereignisse: erfolgreiche und abgelehnte Anfragen, TLS-/JWT-Fehler, Bindungs-Ereignisse, Skript-Fehler. Einträge sind nach Datum, Uhrzeit und Host (IP oder Account-Name) sortiert. Das Protokoll ist die erste Anlaufstelle bei Störungen. Jede Antwort trägt zudem eine Korrelations-ID (Response-Header &#039;&#039;X-Trace-Id&#039;&#039;), die in den Protokoll-Einträgen wiederzufinden ist.&lt;br /&gt;
&lt;br /&gt;
==Statistik==&lt;br /&gt;
&lt;br /&gt;
Die Tabelle &#039;&#039;&#039;RESTSRV_STATS&#039;&#039;&#039; enthält eine Statistik aller bearbeiteten Anfragen&lt;br /&gt;
mit IP, Zugang, Endpunkt, Methode, angefragtem Pfad, HTTP-Status und Laufzeit in&lt;br /&gt;
Millisekunden. Dazu kommen zwei Felder, die den Sprung ins Protokoll erlauben:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;rs_trace&#039;&#039;&#039; - dieselbe Korrelations-ID, die auch im Header &#039;&#039;X-Trace-Id&#039;&#039; und in jeder zugehörigen Protokollzeile steht. Damit findet man zu einer auffälligen Statistikzeile alle Protokolleinträge derselben Anfrage.&lt;br /&gt;
* &#039;&#039;&#039;rs_server&#039;&#039;&#039; - das Server-Profil, über das die Anfrage hereinkam. Bei mehreren Profilen (Public-TLS neben Internal-mTLS) ist das die einzige Angabe, an der sich der Weg ablesen lässt.&lt;br /&gt;
&lt;br /&gt;
Weil der Normalfall keine Protokollzeile mehr schreibt, ist RESTSRV_STATS die&lt;br /&gt;
vollständige Sicht auf den Betrieb - RESTSRV_PROTO die auf die Auffälligkeiten.&lt;br /&gt;
&lt;br /&gt;
Aufruf der Statistik:&lt;br /&gt;
&lt;br /&gt;
* Aus der Endpunkt-Liste: F8 zeigt die Statistik des markierten Endpunkts.&lt;br /&gt;
* Aus der Zugänge-Liste: F8 zeigt die Statistik des markierten Zugangs.&lt;br /&gt;
&lt;br /&gt;
==Konsole==&lt;br /&gt;
&lt;br /&gt;
Wird der REST-Server nicht als Dienst, sondern interaktiv gestartet, erscheint eine&lt;br /&gt;
farbige Konsole mit Live-Log und einem &#039;&#039;&#039;fixierten Kopfbereich&#039;&#039;&#039;, der die gerade&lt;br /&gt;
laufenden Anfragen zeigt und sich fortlaufend aktualisiert.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;show &amp;lt;handle&amp;gt;&#039;&#039; || Details einer Anfrage: Request-Header, Protokollzeilen, Antwort. JSON wird farbig formatiert. &#039;&#039;&amp;lt;handle&amp;gt;&#039;&#039; ist die Kennung aus der Abschlusszeile der Anfrage (z.B. &#039;&#039;7F&#039;&#039;), &#039;&#039;&#039;keine&#039;&#039;&#039; laufende Nummer&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;1&#039;&#039; … &#039;&#039;9&#039;&#039; || Auswahl im Kopfbereich - kürzer als &#039;&#039;show&#039;&#039; für die gerade sichtbaren Anfragen. Mit &#039;&#039;mouse on&#039;&#039; auch per Klick&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;ps&#039;&#039; || Listet die gerade laufenden Anfragen auf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;level &amp;lt;stufe&amp;gt;&#039;&#039; || Anzeigestufe des Live-Logs: &#039;&#039;quiet&#039;&#039; , &#039;&#039;normal&#039;&#039; oder &#039;&#039;debug&#039;&#039;. Ohne Parameter wird die aktuelle Stufe genannt&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;filter &amp;lt;ausdruck&amp;gt;&#039;&#039; || Blendet aus, was nicht passt: &#039;&#039;path=&amp;lt;text&amp;gt;&#039;&#039;, &#039;&#039;account=&amp;lt;name&amp;gt;&#039;&#039;, &#039;&#039;status&amp;gt;=&amp;lt;code&amp;gt;&#039;&#039;. &#039;&#039;filter aus&#039;&#039; hebt die Einschränkung auf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;panel on&amp;amp;#124;off&#039;&#039; || Fixierten Kopfbereich ein-/ausschalten. &#039;&#039;&#039;Ausgeschaltet&#039;&#039;&#039; lässt sich der Rückblick-Puffer des Konsolenfensters wieder scrollen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;mouse on&amp;amp;#124;off&#039;&#039; || Klick-Auswahl im Kopfbereich. &#039;&#039;&#039;Eingeschaltet&#039;&#039;&#039; ist das Markieren von Text mit der Maus nicht mehr möglich&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;help&#039;&#039; / &#039;&#039;?&#039;&#039; || Zeigt diese Liste in der Konsole&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;exit&#039;&#039; / &#039;&#039;quit&#039;&#039; || Beendet den Server geordnet&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Anzeigefilter wirkt &#039;&#039;&#039;nur auf die Anzeige&#039;&#039;&#039;. Was die Konsole&lt;br /&gt;
ausblendet, steht vollständig in RESTSRV_PROTO - es geht nichts verloren.}}&lt;br /&gt;
&lt;br /&gt;
Im Service-Modus (Windows-Dienst) ist die Konsole nicht sichtbar. Alle Einträge landen dort ausschliesslich in der Tabelle RESTSRV_PROTO.&lt;br /&gt;
&lt;br /&gt;
==Weiterführende Seiten==&lt;br /&gt;
&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]] - Schritt-für-Schritt-Anleitung&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]] - TLS, mTLS und Bindungen&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]] - API-Keys, JWT, CORS, Host-Beschränkung&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]] - Verwaltung, Berechtigungen, WebHooks&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]] - Aufbau der Endpunkt-Skripte&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]] - Übertragungsprotokoll für Dateien (Multipart, resumable)&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]] - Dateien ausliefern, Range/Resume, öffentliche Artefakte&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel1|Beispiel 1: Daten-Abruf mit JWT]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel2|Beispiel 2: Zeiterfassung als komplette Webanwendung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel3|Beispiel 3: Datensatz anlegen mit JSON-Body (CRUD)]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4: Pfad-Parameter im Routing]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel5|Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6: Datei-Upload und Ablage im DMS]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=66993</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Einrichtung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=66993"/>
		<updated>2026-10-05T06:34:37Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Automatische Zertifikatserneuerung als Alternative ergaenzt; Angaben zu Root-Zertifikat und Feldbezeichnungen richtiggestellt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Einrichtung eines REST-Endpunktes=&lt;br /&gt;
&lt;br /&gt;
Die folgende Reihenfolge stellt sicher, dass ein lauffähiger Endpunkt mit möglichst wenigen Korrektur-Schleifen entsteht.&lt;br /&gt;
&lt;br /&gt;
==Vorbereitung==&lt;br /&gt;
&lt;br /&gt;
# OBS-Support kontaktieren und das REST-Server-Modul aktivieren lassen.&lt;br /&gt;
# Prüfen, ob auf dem Zielrechner (OBS-Server oder dedizierter REST-Server) Port und IP-Adresse frei sind.&lt;br /&gt;
# Bei TLS-Profilen: TLS-Zertifikat (PEM, mit Kette), privaten Schlüssel (PEM) und - falls mTLS - das CA-Root-Zertifikat bereithalten. Alternativ nutzen Sie die automatische Zertifikatserneuerung; dann genügen ein öffentlicher Name und eine Weiterleitung von Port 80 auf diesen Server.&lt;br /&gt;
&lt;br /&gt;
==Schritt 1: Server-Profil anlegen==&lt;br /&gt;
&lt;br /&gt;
In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server&#039;&#039;&#039; den Punkt &#039;&#039;&#039;Server&#039;&#039;&#039; öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; ein neues Profil anlegen. Pflichtfelder:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Nr&#039;&#039;&#039; - eindeutige Nummer (1-99), wird beim Anlegen automatisch vorgeschlagen.&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - sprechender Name (z.B. &#039;&#039;Public-API&#039;&#039;, &#039;&#039;Internal-mTLS&#039;&#039;, &#039;&#039;Debug-Local&#039;&#039;).&lt;br /&gt;
* Auswahl des Modus:&lt;br /&gt;
** &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039; (Default): Zertifikat und Schlüssel hinterlegen - oder auf der Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039; die automatische Erneuerung aktivieren.&lt;br /&gt;
** &#039;&#039;&#039;mTLS-Modus aktiv&#039;&#039;&#039; setzen: zusätzlich das CA-Root-Zertifikat hinterlegen, gegen das die Client-Zertifikate geprüft werden. Jeder Client muss dann ein gültiges Zertifikat vorlegen.&lt;br /&gt;
** &#039;&#039;&#039;kein SSL&#039;&#039;&#039; setzen: ausschliesslich für lokale Tests, der Server akzeptiert dann nur Plain-HTTP.&lt;br /&gt;
&lt;br /&gt;
Optional lassen sich am Profil eigene &#039;&#039;&#039;Rate-Limit-Schwellen&#039;&#039;&#039; hinterlegen. Ohne&lt;br /&gt;
Angabe gelten die Standardwerte (60-Sekunden-Fenster, 120 erfolgreiche und 10&lt;br /&gt;
fehlgeschlagene Anfragen je Schlüssel, 600 Anfragen je Profil). Für Anwendungen&lt;br /&gt;
ohne eigenen API-Key - typisch mobile Apps - sollten die Werte bewusst gesetzt&lt;br /&gt;
werden, weil dann alle Benutzer hinter einer IP auf denselben Zähler laufen.&lt;br /&gt;
&lt;br /&gt;
Details siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 2: Bindung anlegen==&lt;br /&gt;
&lt;br /&gt;
Im Server-Profil mit &#039;&#039;&#039;F6&#039;&#039;&#039; die Bindungs-Liste öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; eine neue Bindung anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - IP-Adresse, auf der gelauscht werden soll (z.B. &#039;&#039;0.0.0.0&#039;&#039; für alle Adressen, &#039;&#039;127.0.0.1&#039;&#039; für lokal).&lt;br /&gt;
* &#039;&#039;&#039;Port&#039;&#039;&#039; - Port-Nummer (typisch 443 für HTTPS, 8099 für Debug, ggf. abweichend). {{Achtung|Port 8080 wird für RustDesk verwendet und darf daher nicht verwendet werden}}&lt;br /&gt;
* &#039;&#039;&#039;Standard&#039;&#039;&#039; setzen, wenn diese Bindung die Standard-Bindung des Servers ist (pro Server-Profil nur eine).&lt;br /&gt;
&lt;br /&gt;
==Schritt 3: REST-Dienst starten / neustarten==&lt;br /&gt;
&lt;br /&gt;
Damit Änderungen an Server-Profilen oder Bindungen wirksam werden, muss der OBS REST-Server-Dienst (oder die Konsole) gestartet bzw. neugestartet werden. Das Profil wird beim Start aus der Datenbank gelesen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 4: Zugang anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Zugänge&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Zugang anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - erscheint im Protokoll und in der Statistik.&lt;br /&gt;
* &#039;&#039;&#039;API-Key&#039;&#039;&#039; - eindeutig, mit dem Schlüsselsymbol kann ein zufälliger Key generiert werden.&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - optional die IP oder der Hostname des Konsumenten (sollte, wenn möglich, immer gesetzt werden).&lt;br /&gt;
* Optional: &#039;&#039;&#039;CORS-Origins&#039;&#039;&#039; und &#039;&#039;&#039;JWT-Konfiguration&#039;&#039;&#039;, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 5: Endpunkt anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Endpunkte&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Endpunkt anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Endpunkt&#039;&#039;&#039; - Ressourcen-/Anzeigename (z.B. &#039;&#039;orders&#039;&#039;, &#039;&#039;kalender&#039;&#039;); dient nur der Übersicht, nicht dem Routing.&lt;br /&gt;
* &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; - der maßgebliche Routing-Pfad, ggf. mit Platzhaltern (z.B. &#039;&#039;/orders&#039;&#039;, &#039;&#039;/orders/{uid}&#039;&#039;, &#039;&#039;/orders/{uid}/modules/{code}&#039;&#039;).&lt;br /&gt;
* &#039;&#039;&#039;Server&#039;&#039;&#039; - die Server-Auswahl bestimmt, über welches Server-Profil der Endpunkt erreichbar ist.&lt;br /&gt;
* &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; - nur setzen, wenn der Endpunkt Dateien entgegennehmen soll; dann auch die &#039;&#039;&#039;Max. Upload-Grösse&#039;&#039;&#039; in MB festlegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6]]).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Eine Versionskennung wird als statisches Segment ins Pfad-Template aufgenommen (z.B. &#039;&#039;/orders/v1&#039;&#039;); ein eigenes Versionsfeld gibt es nicht. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].}}&lt;br /&gt;
&lt;br /&gt;
Anschliessend mit &#039;&#039;&#039;F7&#039;&#039;&#039; das Endpunkt-Skript pflegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]]).&lt;br /&gt;
&lt;br /&gt;
==Schritt 6: Zugang für den Endpunkt freischalten==&lt;br /&gt;
&lt;br /&gt;
Aus der Endpunkt-Liste den neuen Endpunkt markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die Berechtigungs-Liste. Mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; kann ein oder mehrere Zugänge ausgewählt und mit &#039;&#039;&#039;F2&#039;&#039;&#039; übernommen werden. Erst nach dieser Freischaltung darf ein Zugang den Endpunkt aufrufen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 7: Funktionsprüfung==&lt;br /&gt;
&lt;br /&gt;
Aufruf mit einem HTTP-Client (curl, Postman, Browser-Plug-in):&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; https://api.meinserver.de/[Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Bei korrektem Setup erscheint die JSON-Antwort des Skripts. Fehlerfälle werden in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; protokolliert.&lt;br /&gt;
&lt;br /&gt;
==Schritt 8: Statistik und Protokoll prüfen==&lt;br /&gt;
&lt;br /&gt;
* In der Endpunkt-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Endpunkts.&lt;br /&gt;
* In der Zugänge-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Zugangs.&lt;br /&gt;
* In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Protokoll&#039;&#039;&#039; die Ereignisse prüfen.&lt;br /&gt;
&lt;br /&gt;
==Wartung der Server-Tabellen==&lt;br /&gt;
&lt;br /&gt;
Die Tabellen des REST-Servers pflegen sich selbst; ein regelmässiger Eingriff ist&lt;br /&gt;
nicht vorgesehen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Tabelle !! Aufräumen&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_IDEMPOTENCY || Abgeschlossene Einträge werden nach 30 Tagen gelöscht. Offene Reservierungen werden ab 24 Stunden im Protokoll gemeldet und &#039;&#039;&#039;anschliessend ebenfalls gelöscht&#039;&#039;&#039; - sie sind trotzdem ein Support-Fall, weil die Fachwirkung unbekannt ist, siehe Stolpersteine&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_TOKEN || Zeilen werden gelöscht, sobald Access- &#039;&#039;&#039;und&#039;&#039;&#039; Refresh-Laufzeit vorbei sind. Läuft beim Serverstart und danach höchstens stündlich&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_PROTO / RESTSRV_STATS || Wachsen mit dem Betrieb. Bei hohem Aufkommen empfiehlt sich ein turnusmässiges Archivieren&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Häufige Stolpersteine==&lt;br /&gt;
&lt;br /&gt;
* Bindung angelegt, aber der Server wurde nicht neugestartet -&amp;gt; die Bindung ist noch nicht aktiv.&lt;br /&gt;
* Endpunkt einem falschen Server zugeordnet -&amp;gt; Aufruf liefert 404 &#039;&#039;Endpunkt nicht vorhanden oder inaktiv&#039;&#039;.&lt;br /&gt;
* Pfad-Template falsch geschrieben oder Segmentzahl passt nicht -&amp;gt; kein Template matcht, Aufruf liefert 404.&lt;br /&gt;
* Zugang für den Endpunkt nicht freigeschaltet -&amp;gt; Aufruf liefert 403 &#039;&#039;Keine Berechtigung für die Endpunkt-Nutzung&#039;&#039;.&lt;br /&gt;
* Falscher API-Key oder falscher Host beim Zugang -&amp;gt; Aufruf liefert 401 bzw. 403.&lt;br /&gt;
* mTLS aktiv, aber kein Client-Zertifikat installiert -&amp;gt; Verbindung wird während des TLS-Handshakes abgebrochen.&lt;br /&gt;
* Datei-Upload ohne gesetzten Haken &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; am Endpunkt -&amp;gt; Aufruf liefert 415, das Skript läuft nicht an.&lt;br /&gt;
* Skript geändert, aber der Aufruf verhält sich noch wie vorher -&amp;gt; Endpunkt-Cache abwarten (bis zu 60 Sekunden).&lt;br /&gt;
* Schreibaufruf (POST/PUT/PATCH/DELETE) liefert 400 &#039;&#039;IDEMPOTENCY_KEY_MISSING&#039;&#039; -&amp;gt; der Client sendet keinen &#039;&#039;Idempotency-Key&#039;&#039;. Der Header ist seit v1.21 &#039;&#039;&#039;Pflicht&#039;&#039;&#039;, nicht optional; das Endpunkt-Skript läuft in diesem Fall gar nicht an. Betrifft typischerweise Konsumenten, die vor dem Update gebaut wurden.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 422 &#039;&#039;IDEMPOTENCY_KEY_REUSED&#039;&#039; -&amp;gt; der Client sendet denselben &#039;&#039;Idempotency-Key&#039;&#039; mit geändertem Inhalt. Pro Vorgang genau ein Schlüssel, über alle Wiederholversuche hinweg unverändert.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 409 &#039;&#039;IDEMPOTENCY_UNRESOLVED&#039;&#039; -&amp;gt; ein früherer Versuch wurde hart abgebrochen (Dienst beendet). Ob gebucht wurde, ist offen: im Protokoll und in den Fachdaten prüfen. Der Aufräumlauf meldet den Eintrag nach 24 Stunden im Protokoll und löscht ihn dann; wer schneller weitermachen will, entfernt ihn in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;.&lt;br /&gt;
* Nach einem Update antworten &#039;&#039;&#039;alle&#039;&#039;&#039; bestehenden Token mit 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; -&amp;gt; erwartetes Verhalten. Token, die vor der Einführung der Sitzungsprüfung ausgestellt wurden, gehören zu keiner Sitzung; die Konsumenten müssen sich einmalig neu anmelden.&lt;br /&gt;
* Anmeldung liefert 503 statt eines Tokens -&amp;gt; die Sitzungszeile konnte nicht geschrieben werden. Ursache im Protokoll unter &#039;&#039;TokenStore: ...&#039;&#039; nachsehen; typisch ist eine fehlende Tabelle &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; nach einem Update ohne Datenbank-Abgleich.&lt;br /&gt;
* Abmelden liefert 503 -&amp;gt; die Sitzung liess sich nicht sperren. Der Client ist &#039;&#039;&#039;nicht&#039;&#039;&#039; abgemeldet und muss den Aufruf wiederholen; Details im Protokoll.&lt;br /&gt;
* Techniker wird beim Arbeiten unerwartet abgemeldet -&amp;gt; im Protokoll nach &#039;&#039;WIEDERVORLAGE&#039;&#039; suchen. Ein doppelt verwendetes Refresh-Token sperrt die Sitzung. Legt die App Token mehrfach parallel ab, gehört das clientseitig behoben.&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=66992</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Server-Profile</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=66992"/>
		<updated>2026-10-05T06:34:36Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Abschnitt zur automatischen Zertifikatserneuerung ergaenzt; Root-Zertifikat richtiggestellt (nur bei mTLS, nicht Teil der Serverkette); Feldbezeichnungen an die Maske angeglichen [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Server-Profile=&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Server&#039;&#039;&#039; im REST-Modul ist ein eigenständiges TLS-Profil mit einer eigenen HTTP-Server-Instanz. In OBS können beliebig viele Server-Profile parallel betrieben werden, jedes mit eigenem Modus, eigenen Zertifikaten und eigenen Bindungen.&lt;br /&gt;
&lt;br /&gt;
Typische Einsatzfälle für mehrere Profile:&lt;br /&gt;
&lt;br /&gt;
* Ein &#039;&#039;&#039;Public-Profil&#039;&#039;&#039; mit Standard-TLS auf Port 443 für externe Konsumenten.&lt;br /&gt;
* Ein &#039;&#039;&#039;Internal-mTLS-Profil&#039;&#039;&#039; auf einem internen Port für Microservices, die zwingend ein Client-Zertifikat vorlegen müssen.&lt;br /&gt;
* Ein &#039;&#039;&#039;Debug-Profil&#039;&#039;&#039; mit Plain-HTTP auf 127.0.0.1, ausschliesslich für lokale Tests.&lt;br /&gt;
&lt;br /&gt;
==Aufruf==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Server&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Felder des Server-Profils==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Nr                  || Eindeutige Nummer 1-99, einmalig vergeben, kann später nicht mehr geändert werden&lt;br /&gt;
|-&lt;br /&gt;
| Name                || Sprechender Name, eindeutig - erscheint in Protokoll, Statistik und Endpunkt-Zuordnung&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv               || Profil wird beim Start des REST-Dienstes geladen. Inaktive Profile starten nicht&lt;br /&gt;
|-&lt;br /&gt;
| zusätzliche Infos   || Freitext zur Dokumentation (interner Kommentar, wird nicht ausgewertet)&lt;br /&gt;
|-&lt;br /&gt;
| kein SSL            || Plain-HTTP-Modus, ausschliesslich für Debug. TLS-Felder werden ignoriert&lt;br /&gt;
|-&lt;br /&gt;
| mTLS-Modus aktiv    || Mutual TLS: jeder Client muss ein gültiges Client-Zertifikat vorlegen&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikat (cert)   || PEM-Inhalt des Server-Zertifikats (inkl. ggf. Zwischen-CA-Kette)&lt;br /&gt;
|-&lt;br /&gt;
| Schlüssel (key)     || PEM-Inhalt des privaten Schlüssels&lt;br /&gt;
|-&lt;br /&gt;
| Root-Zertifikat (ca) || PEM-Inhalt des CA-Zertifikats, gegen das &#039;&lt;br /&gt;
|-&lt;br /&gt;
| Rate-Limit-Fenster  || Länge des Rate-Limit-Fensters in Sekunden. Leer/0 = Standard 60&lt;br /&gt;
|-&lt;br /&gt;
| Erfolgsanfragen     || Erlaubte erfolgreiche Anfragen je Schlüssel und Fenster. Leer/0 = Standard 120&lt;br /&gt;
|-&lt;br /&gt;
| Fehlanfragen        || Erlaubte fehlgeschlagene Anfragen je Schlüssel und Fenster. Leer/0 = Standard 10&lt;br /&gt;
|-&lt;br /&gt;
| Anfragen            || Erlaubte Anfragen des gesamten Profils je Fenster. Leer/0 = Standard 600&lt;br /&gt;
|-&lt;br /&gt;
| max. Verbindungen   || Obergrenze gleichzeitiger &#039;&#039;&#039;Verbindungen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_conn&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Sie deckt auch laufende Datei-Downloads ab, weil der Platz erst beim Trennen frei wird. Bei Überschreitung trennt der Server &#039;&#039;&#039;ohne HTTP-Antwort&#039;&#039;&#039; - der Wert gehört deutlich über den Normalbetrieb, als Reissleine&lt;br /&gt;
|-&lt;br /&gt;
| max. Anfragen       || Obergrenze gleichzeitig &#039;&#039;&#039;verarbeiteter Anfragen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_parallel&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Bei Überschreitung antwortet der Server &#039;&#039;&#039;503&#039;&#039;&#039; mit &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;, der Client darf es also gleich erneut versuchen&lt;br /&gt;
|-&lt;br /&gt;
| keine Kompression   || Antworten dieses Profils werden &#039;&#039;&#039;nicht&#039;&#039;&#039; gepackt (&amp;lt;code&amp;gt;rsv_no_gzip&amp;lt;/code&amp;gt;). Ohne Haken packt der Server JSON- und Textantworten ab 1024 Byte mit gzip, sofern der Client sie über &amp;lt;code&amp;gt;Accept-Encoding&amp;lt;/code&amp;gt; annimmt. Der Haken betrifft nur die Antwortrichtung - komprimierte &#039;&#039;&#039;Anfragen&#039;&#039;&#039; nimmt der Server weiterhin entgegen&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;max. Verbindungen&#039;&#039; und &#039;&#039;max. Anfragen&#039;&#039; sind die beiden&lt;br /&gt;
&#039;&#039;&#039;Lastgrenzen&#039;&#039;&#039; des Profils. Davon zu unterscheiden sind die festen Grenzen der&lt;br /&gt;
persistenten Verbindungen - 10 Sekunden Leerlauf, 100 Anfragen je Verbindung -,&lt;br /&gt;
die nicht der Last dienen, sondern der Hygiene und nicht einstellbar sind. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer|REST-Server]], Abschnitt Persistente&lt;br /&gt;
Verbindungen.}}&lt;br /&gt;
&lt;br /&gt;
Die Inhalte der Zertifikate werden direkt in OBS als Text gespeichert. Zur Laufzeit schreibt der Server die PEM-Inhalte für den Bruchteil einer Sekunde in ein Temp-Verzeichnis, damit OpenSSL sie beim Start lesen kann; danach werden die Temp-Dateien sofort wieder gelöscht.&lt;br /&gt;
&lt;br /&gt;
==Modi==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Modus               !! Voraussetzungen                       !! Anwendung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039;  || Zertifikat + Schlüssel                || Empfohlener Default für alle Server, die über das öffentliche Netz erreichbar sind&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;mTLS&#039;&#039;&#039;          || Zertifikat + Schlüssel + Root-Zert.   || Maschine-zu-Maschine-Kommunikation, in der jeder Client mit Cert validiert wird&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Plain-HTTP&#039;&#039;&#039;    || Haken &#039;&#039;kein SSL&#039;&#039;                    || Ausschliesslich lokale Tests. Erkennt TLS-Versuche auf dem Plain-Port und trennt sie direkt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beide Richtungen werden erkannt: Eine &#039;&#039;&#039;unverschlüsselte Anfrage an einen&lt;br /&gt;
TLS-Port&#039;&#039;&#039; (&#039;&#039;http&#039;&#039; statt &#039;&#039;https&#039;&#039;) beantwortet der Server mit &#039;&#039;&#039;HTTP 400&#039;&#039;&#039;&lt;br /&gt;
und einem Klartext-Hinweis. Die Abweisung erfolgt &#039;&#039;&#039;vor&#039;&#039;&#039; Authentifizierung,&lt;br /&gt;
Rate-Limit und Endpunkt-Skript - ein falsch konfigurierter Client löst also&lt;br /&gt;
keine Verarbeitung aus.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Bei Standard-TLS und mTLS fährt der Server ohne Zertifikat und Schlüssel gar nicht erst hoch. Bei aktiver Erneuerung trägt der Dienst beides selbst ein. Plain-HTTP umgeht TLS komplett und ist nicht für Produktivumgebungen geeignet.}}&lt;br /&gt;
&lt;br /&gt;
==TLS-Version, Cipher und Zertifikats-Rotation==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TLS-Version:&#039;&#039;&#039; Der Server erzwingt &#039;&#039;&#039;TLS 1.2&#039;&#039;&#039;. Die Cipher-Liste ist auf ECDHE-Schlüsselaustausch (Forward Secrecy) mit AEAD-Verfahren (AES-GCM, ChaCha20-Poly1305) beschränkt; CBC, statisches RSA-KEX und DHE sind ausgeschlossen.&lt;br /&gt;
* &#039;&#039;&#039;HSTS:&#039;&#039;&#039; Jede Antwort eines TLS-Profils trägt &#039;&#039;Strict-Transport-Security: max-age=31536000; includeSubDomains&#039;&#039;. Ein Browser spricht die Adresse damit ein Jahr lang nur noch über HTTPS an. Beim Profil &#039;&#039;Kein SSL&#039;&#039; wird der Header bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; gesetzt - der Browser würde ihn sonst speichern und die Erzwingung auch auf produktive Sitzungen anwenden.&lt;br /&gt;
* &#039;&#039;&#039;Rotation:&#039;&#039;&#039; Bei &#039;&#039;&#039;automatischer Erneuerung&#039;&#039;&#039; tauscht der Dienst das Zertifikat selbst und startet dabei nur das betroffene Server-Profil neu; die übrigen Profile und der Dienst laufen weiter. Bei manueller Pflege ändern Sie die PEM-Inhalte und starten den REST-Dienst neu - bis dahin läuft das alte Zertifikat weiter.&lt;br /&gt;
* &#039;&#039;&#039;Certificate-Pinning:&#039;&#039;&#039; Mobile Clients können den Server-Public-Key pinnen. Zertifikatswechsel daher &#039;&#039;&#039;rechtzeitig ankündigen&#039;&#039;&#039; und mit einem &#039;&#039;&#039;Backup-Pin&#039;&#039;&#039; vorbereiten - der neue Pin muss vor dem Wechsel in einer Client-Version ausgerollt sein, sonst sperrt sich die App aus.&lt;br /&gt;
==Automatische Zertifikatserneuerung==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server kann sein TLS-Zertifikat selbst bei einer Zertifizierungsstelle beantragen und vor Ablauf erneuern. Zertifikat und privater Schlüssel entstehen dabei auf Ihrem Server und verlassen ihn nicht.&lt;br /&gt;
&lt;br /&gt;
===Voraussetzungen===&lt;br /&gt;
&lt;br /&gt;
* Ein öffentlicher Name, der von aussen auf diesen Server zeigt.&lt;br /&gt;
* &#039;&#039;&#039;Port 80&#039;&#039;&#039; ist von aussen auf diesen Server weitergeleitet. Darüber prüft die Zertifizierungsstelle, dass Sie den Namen betreiben. Der Dienst öffnet den Port nur für die Dauer der Prüfung und schliesst ihn danach wieder.&lt;br /&gt;
* Ausgehende HTTPS-Verbindungen zur Zertifizierungsstelle sind erlaubt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Port 80 wird unabhängig davon benötigt, auf welchem Port der REST-Server selbst lauscht. Die Prüfung der Zertifizierungsstelle beginnt immer auf Port 80.}}&lt;br /&gt;
&lt;br /&gt;
===Felder===&lt;br /&gt;
&lt;br /&gt;
Registerkarte &#039;&#039;&#039;4 Zertifikaterneuerung&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                       !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Automatische Erneuerung    || &#039;&#039;deaktiviert&#039;&#039; = aus. &#039;&#039;Produktivbetrieb&#039;&#039; = der Dienst beantragt und erneuert das Zertifikat selbst. &#039;&#039;Testbetrieb&#039;&#039; = siehe Warnung unten&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikatsname (FQDN)     || Der Name, auf den das Zertifikat ausgestellt wird. Pflicht, sobald die Erneuerung aktiv ist&lt;br /&gt;
|-&lt;br /&gt;
| Kontakt bei der CA         || Optional. Die Zertifizierungsstelle schickt an diese Adresse Warnungen, wenn ein Zertifikat abzulaufen droht&lt;br /&gt;
|-&lt;br /&gt;
| Zertifikat gültig bis      || Ablaufdatum des aktiven Zertifikats&lt;br /&gt;
|-&lt;br /&gt;
| Testzertifikat aus Staging || Gesetzt, wenn das aktive Zertifikat aus dem Testbetrieb stammt&lt;br /&gt;
|-&lt;br /&gt;
| Zustand                    || &#039;&#039;idle&#039;&#039; = nichts zu tun, &#039;&#039;pending&#039;&#039; = Vorgang läuft, &#039;&#039;error&#039;&#039; = letzter Versuch fehlgeschlagen&lt;br /&gt;
|-&lt;br /&gt;
| Letzte Meldung             || Ergebnis des letzten Vorgangs - im Störungsfall das erste Feld, in das Sie sehen&lt;br /&gt;
|-&lt;br /&gt;
| Gesperrt bis               || Nach einem Fehlschlag wartet der Dienst bis zu diesem Zeitpunkt, bevor er es erneut versucht&lt;br /&gt;
|-&lt;br /&gt;
| Letzter Selbsttest am      || Zeitpunkt der letzten Erreichbarkeitsprüfung&lt;br /&gt;
|-&lt;br /&gt;
| Selbsttest bestanden       || Ergebnis der letzten Erreichbarkeitsprüfung&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die Felder ab &#039;&#039;Zertifikat gültig bis&#039;&#039; füllt der Dienst selbst; sie dienen der Anzeige.&lt;br /&gt;
&lt;br /&gt;
===Ablauf===&lt;br /&gt;
&lt;br /&gt;
Der Dienst sieht stündlich nach, ob etwas ansteht:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Selbsttest:&#039;&#039;&#039; Einmal wöchentlich prüft der Dienst über das Testsystem der Zertifizierungsstelle, ob Ihr Server von aussen erreichbar ist. Besteht der Test nicht, beantragt der Dienst kein Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Erneuerung:&#039;&#039;&#039; Ab 30 Tagen Restlaufzeit beantragt der Dienst ein neues Zertifikat.&lt;br /&gt;
# &#039;&#039;&#039;Übernahme:&#039;&#039;&#039; Der Dienst prüft das neue Zertifikat, speichert es und bewahrt das bisherige als Rückfall auf. Anschliessend startet &#039;&#039;&#039;nur das betroffene Server-Profil&#039;&#039;&#039; neu.&lt;br /&gt;
# &#039;&#039;&#039;Rückfall:&#039;&#039;&#039; Startet das Profil mit dem neuen Zertifikat nicht, aktiviert der Dienst wieder das vorherige.&lt;br /&gt;
&lt;br /&gt;
Nach einem Fehlschlag wartet der Dienst zunehmend länger, bevor er es erneut versucht - eine, zwei, vier, acht und höchstens 24 Stunden. Das verhindert, dass ein dauerhafter Fehler das Anfragekontingent der Zertifizierungsstelle aufbraucht.&lt;br /&gt;
&lt;br /&gt;
===Befehle in der Konsole===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert&amp;lt;/code&amp;gt; || Zeigt je Profil Betriebsart, Ablaufdatum, Zustand und das Ergebnis des letzten Selbsttests&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert test&amp;lt;/code&amp;gt; || Prüft sofort, ob der Server von aussen erreichbar ist. Beantragt kein Zertifikat&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;cert renew&amp;lt;/code&amp;gt; || Stösst die Erneuerung sofort an, auch wenn sie noch nicht fällig ist&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Testbetrieb===&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Im &#039;&#039;&#039;Testbetrieb&#039;&#039;&#039; beantragt der Dienst das Zertifikat beim Testsystem der Zertifizierungsstelle. Einem solchen Zertifikat vertraut &#039;&#039;&#039;kein&#039;&#039;&#039; Client - Anwendungen können sich danach nicht mehr mit diesem Server verbinden. Verwenden Sie den Testbetrieb ausschliesslich auf Testsystemen. Zurück in den Produktivbetrieb kommen Sie, indem Sie das Feld &#039;&#039;&#039;Automatische Erneuerung&#039;&#039;&#039; auf &#039;&#039;Produktivbetrieb&#039;&#039; stellen; der Dienst holt daraufhin sofort ein gültiges Zertifikat, auch wenn das Testzertifikat noch lange läuft.}}&lt;br /&gt;
&lt;br /&gt;
==Rate-Limit je Profil==&lt;br /&gt;
&lt;br /&gt;
Jedes Server-Profil führt &#039;&#039;&#039;eigene Zähler&#039;&#039;&#039; und kann &#039;&#039;&#039;eigene Schwellen&#039;&#039;&#039; haben.&lt;br /&gt;
Ein Profil, das gerade überrannt wird, bremst damit die anderen Profile nicht mit&lt;br /&gt;
aus - das Public-Profil und das interne mTLS-Profil sind voneinander unabhängig.&lt;br /&gt;
&lt;br /&gt;
Gezählt wird pro Schlüssel, und der Schlüssel ist der &#039;&#039;&#039;API-Key&#039;&#039;&#039; des Zugangs.&lt;br /&gt;
Sendet ein Konsument keinen API-Key (Zugang mit &#039;&#039;*&#039;&#039;), wird ersatzweise auf die&lt;br /&gt;
&#039;&#039;&#039;IP-Adresse&#039;&#039;&#039; gezählt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Das ist der wichtigste Punkt bei mobilen Anwendungen: Sitzen alle&lt;br /&gt;
Benutzer hinter einer gemeinsamen Firmen-IP und sendet die App keinen eigenen&lt;br /&gt;
API-Key, teilen sich &#039;&#039;&#039;alle Benutzer einen Zähler&#039;&#039;&#039;. Mit den Standardwerten sind&lt;br /&gt;
das 10 Fehlversuche für den ganzen Betrieb pro Minute - nach ein paar falschen&lt;br /&gt;
Passworteingaben steht die Abteilung. Für solche Profile die Schwellen bewusst&lt;br /&gt;
höher setzen oder jedem Konsumenten einen eigenen API-Key geben.}}&lt;br /&gt;
&lt;br /&gt;
Bleiben alle vier Felder leer (bzw. 0), gelten die Standardwerte. Die Werte werden&lt;br /&gt;
&#039;&#039;&#039;beim Start&#039;&#039;&#039; des REST-Dienstes gelesen - eine Änderung wird erst nach einem&lt;br /&gt;
Neustart wirksam.&lt;br /&gt;
&lt;br /&gt;
==Validierung beim Speichern==&lt;br /&gt;
&lt;br /&gt;
Immer geprüft wird:&lt;br /&gt;
&lt;br /&gt;
* Nr und Name sind vergeben, Nr liegt zwischen 1 und 99.&lt;br /&gt;
* Nr und Name sind eindeutig.&lt;br /&gt;
* Eingetragene Zertifikate und Schlüssel liegen im PEM-Format vor.&lt;br /&gt;
* Ein eingetragener Zertifikatsname enthält keine Adresse, keinen Port und keine Leerzeichen.&lt;br /&gt;
&lt;br /&gt;
Nur bei &#039;&#039;&#039;aktivem&#039;&#039;&#039; Profil zusätzlich:&lt;br /&gt;
&lt;br /&gt;
* Ohne &#039;&#039;kein SSL&#039;&#039; und ohne automatische Erneuerung sind Zertifikat und Schlüssel vorhanden.&lt;br /&gt;
* Bei &#039;&#039;mTLS-Modus aktiv&#039;&#039; ist das Root-Zertifikat vorhanden.&lt;br /&gt;
* Bei aktiver Erneuerung ist der Zertifikatsname vergeben, und &#039;&#039;kein SSL&#039;&#039; ist nicht gesetzt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein &#039;&#039;&#039;nicht aktives&#039;&#039;&#039; Profil dürfen Sie unvollständig speichern - es wird beim Start des Dienstes nicht geladen. Was Sie eintragen, wird trotzdem auf Plausibilität geprüft.}}&lt;br /&gt;
&lt;br /&gt;
==Bindungen==&lt;br /&gt;
&lt;br /&gt;
Eine Bindung ist die Kombination aus IP-Adresse und Port, auf der ein Server-Profil lauscht. Pro Server-Profil können mehrere Bindungen existieren, z.B. um die gleiche TLS-Konfiguration parallel auf 443 und 8443 anzubieten.&lt;br /&gt;
&lt;br /&gt;
===Aufruf===&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste den gewünschten Server markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die zum Server gehörende Bindungs-Liste.&lt;br /&gt;
&lt;br /&gt;
===Felder===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld    !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Host    || IP-Adresse oder Hostname, auf dem gelauscht wird. &#039;&#039;0.0.0.0&#039;&#039; = alle IPv4-Adressen; &#039;&#039;127.0.0.1&#039;&#039; = nur lokal; explizite IPs binden gezielt auf eine Netzwerkkarte&lt;br /&gt;
|-&lt;br /&gt;
| Port    || TCP-Port (1-65535). 443/8443 für HTTPS, frei wählbar für Debug-Profile&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv   || Bindung wird beim Server-Start geladen&lt;br /&gt;
|-&lt;br /&gt;
| Standard|| Markiert die Standard-Bindung des Servers. Pro Server-Profil ist nur eine Standard-Bindung erlaubt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Wirksamkeit===&lt;br /&gt;
&lt;br /&gt;
Bindungen werden ausschliesslich beim Start des REST-Dienstes geladen. Änderungen werden daher erst nach einem Neustart aktiv. Bis dahin laufen die alten Bindungen weiter.&lt;br /&gt;
&lt;br /&gt;
==Listen-Funktionen==&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfg&#039;&#039;&#039; - neues Server-Profil&lt;br /&gt;
* &#039;&#039;&#039;Return&#039;&#039;&#039; - vorhandenes Profil bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F4&#039;&#039;&#039; - Sortierung wählen&lt;br /&gt;
* &#039;&#039;&#039;F6&#039;&#039;&#039; - Bindungs-Liste des markierten Servers&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Datei-Upload&amp;diff=65547</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Datei-Upload</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Datei-Upload&amp;diff=65547"/>
		<updated>2026-09-25T09:28:34Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Reader-Aufrufe statt _OBS_UPLOAD_-Felder, Idempotency-Key-Pflicht bei Uploads ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
= Datei-Upload =&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt nimmt Dateien nur entgegen, wenn er dafür freigeschaltet ist&lt;br /&gt;
(&amp;lt;code&amp;gt;re_upload = 1&amp;lt;/code&amp;gt;, siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]). Dann gilt nicht&lt;br /&gt;
das JSON-Body-Limit von 10&amp;amp;nbsp;MB, sondern die pro Endpunkt konfigurierte Grösse&lt;br /&gt;
(&amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;, Standard 25&amp;amp;nbsp;MB; Überschreitung -&amp;gt; &#039;&#039;&#039;413&#039;&#039;&#039;).&lt;br /&gt;
&lt;br /&gt;
Diese Seite beschreibt die &#039;&#039;&#039;Übertragung&#039;&#039;&#039; - was ein Client senden muss und was&lt;br /&gt;
der Server antwortet. Was das &#039;&#039;&#039;Skript&#039;&#039;&#039; davon sieht, sind vier Aufrufe&lt;br /&gt;
(&amp;lt;code&amp;gt;oReader.UploadPath()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;UploadName()&amp;lt;/code&amp;gt;,&lt;br /&gt;
&amp;lt;code&amp;gt;UploadType()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;UploadSha()&amp;lt;/code&amp;gt;) und steht unter&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting#Datei-Uploads|Scripting]].&lt;br /&gt;
Ein vollständiges Beispiel zeigt&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6]].&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Server legt die Datei in einem temporären Verzeichnis ab und&lt;br /&gt;
&#039;&#039;&#039;verschiebt sie nicht selbst&#039;&#039;&#039;. Übernimmt das Skript sie nicht an ihren&lt;br /&gt;
Zielort, bleibt sie dort liegen.}}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Ein Upload läuft über &#039;&#039;POST&#039;&#039; und braucht deshalb - wie jeder&lt;br /&gt;
schreibende Aufruf - den Header &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt;. Fehlt er, antwortet&lt;br /&gt;
der Server mit &#039;&#039;&#039;400&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt;, und zwar bevor eine&lt;br /&gt;
Datei entgegengenommen wird. Beim resumable Upload gehört er an &#039;&#039;&#039;jedes&#039;&#039;&#039;&lt;br /&gt;
Teilstück; wirksam wird er beim letzten, weil erst dort eine Antwort entsteht, die&lt;br /&gt;
sich speichern lässt. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]], Abschnitt&lt;br /&gt;
Idempotenz.}}&lt;br /&gt;
&lt;br /&gt;
== Einfacher Upload (multipart/form-data) ==&lt;br /&gt;
&lt;br /&gt;
Eine Datei pro Request. Name und Content-Type stammen aus der&lt;br /&gt;
&amp;lt;code&amp;gt;Content-Disposition&amp;lt;/code&amp;gt; der Datei-Partie (&amp;lt;code&amp;gt;filename=&amp;lt;/code&amp;gt;). Der&lt;br /&gt;
Server speichert die erste Datei-Partie und ruft das Skript auf.&lt;br /&gt;
&lt;br /&gt;
Die &#039;&#039;&#039;übrigen Partien&#039;&#039;&#039; des Formulars - alles ohne &amp;lt;code&amp;gt;filename=&amp;lt;/code&amp;gt; -&lt;br /&gt;
liest das Skript über &amp;lt;code&amp;gt;oReader.Form(&#039;&amp;amp;lt;name&amp;amp;gt;&#039;)&amp;lt;/code&amp;gt;. Der Wert ist UTF-8 und&lt;br /&gt;
auf 1024 Zeichen begrenzt, der Name auf Buchstaben, Ziffern und Unterstrich&lt;br /&gt;
reduziert. Zusatzangaben lassen sich damit im selben Request mitschicken, ohne sie&lt;br /&gt;
an die URL zu hängen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cPfad : string;&lt;br /&gt;
    cName : string;&lt;br /&gt;
    cTyp  : string;&lt;br /&gt;
    cBemerk: string;&lt;br /&gt;
begin&lt;br /&gt;
    cPfad   := oReader.UploadPath();&lt;br /&gt;
    cName   := oReader.UploadName();&lt;br /&gt;
    cTyp    := oReader.UploadType();&lt;br /&gt;
    cBemerk := oReader.Form(&#039;bemerkung&#039;);   // Formularfeld derselben Uebertragung&lt;br /&gt;
&lt;br /&gt;
    // ... Datei aus cPfad ins DMS / Zielverzeichnis uebernehmen ...&lt;br /&gt;
&lt;br /&gt;
    oWriter.Str(&#039;status&#039;   , &#039;ok&#039;);&lt;br /&gt;
    oWriter.Str(&#039;dateiname&#039;, cName);&lt;br /&gt;
    oWriter.Status(201);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Resumable/Chunked Upload (Content-Range) ==&lt;br /&gt;
&lt;br /&gt;
Für grosse Dateien überträgt der Client die Datei in Teilstücken (Chunks) mit dem&lt;br /&gt;
Header &amp;lt;code&amp;gt;Content-Range: bytes START-END/TOTAL&amp;lt;/code&amp;gt;. Der Server hängt die&lt;br /&gt;
Chunks &#039;&#039;&#039;sequentiell&#039;&#039;&#039; an eine Session-Datei an und behandelt unvollständige&lt;br /&gt;
Uploads selbst - das Endpunkt-Skript läuft &#039;&#039;&#039;erst beim vollständigen Upload&#039;&#039;&#039;,&lt;br /&gt;
und dann liefern &#039;&#039;oReader.UploadPath()&#039;&#039;, &#039;&#039;UploadName()&#039;&#039; und &#039;&#039;UploadType()&#039;&#039;&lt;br /&gt;
dasselbe wie beim einfachen Upload. Für die Teilstücke läuft es &#039;&#039;&#039;nicht&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Name und Content-Type sind im &amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;-Protokoll nicht enthalten;&lt;br /&gt;
der Client liefert sie deshalb &#039;&#039;&#039;im ersten Chunk&#039;&#039;&#039; über den Header&lt;br /&gt;
&amp;lt;code&amp;gt;Upload-Metadata&amp;lt;/code&amp;gt; (Format wie bei tus: kommagetrennte Paare&lt;br /&gt;
&amp;lt;code&amp;gt;schlüssel base64wert&amp;lt;/code&amp;gt;, Werte Base64-kodiert):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Upload-Metadata: filename ZmlsZS5wZGY=,filetype YXBwbGljYXRpb24vcGRm&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Erkannt werden die Schlüssel &amp;lt;code&amp;gt;filename&amp;lt;/code&amp;gt; (Pflicht) und&lt;br /&gt;
&amp;lt;code&amp;gt;filetype&amp;lt;/code&amp;gt; (optional). Fehlt &amp;lt;code&amp;gt;filename&amp;lt;/code&amp;gt; im ersten Chunk,&lt;br /&gt;
wird der Upload mit &#039;&#039;&#039;400&#039;&#039;&#039; abgelehnt, ohne dass eine Datei angelegt wird.&lt;br /&gt;
Folge-Chunks und Statusabfragen müssen die Metadaten nicht erneut senden.&lt;br /&gt;
&lt;br /&gt;
Ablauf (vom Client gesteuert, der Server antwortet jeweils):&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Request !! Server-Antwort&lt;br /&gt;
|-&lt;br /&gt;
| Erster Chunk &amp;lt;code&amp;gt;Content-Range: bytes 0-1048575/5000000&amp;lt;/code&amp;gt; + &amp;lt;code&amp;gt;Upload-Metadata&amp;lt;/code&amp;gt; || &#039;&#039;&#039;308&#039;&#039;&#039; Resume Incomplete + Header &amp;lt;code&amp;gt;Upload-Id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Upload-Offset&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| weitere Chunks (jeweils &amp;lt;code&amp;gt;Upload-Id&amp;lt;/code&amp;gt; mitsenden) || &#039;&#039;&#039;308&#039;&#039;&#039; mit aktualisiertem &amp;lt;code&amp;gt;Upload-Offset&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| letzter Chunk (Bereich erreicht TOTAL) || normale Skript-Antwort (z.B. &#039;&#039;&#039;200&#039;&#039;&#039;/&#039;&#039;&#039;201&#039;&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| Statusabfrage &amp;lt;code&amp;gt;Content-Range: bytes */5000000&amp;lt;/code&amp;gt; || aktueller &amp;lt;code&amp;gt;Upload-Offset&amp;lt;/code&amp;gt;, ohne anzuhängen (Resume)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Header !! Richtung !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| Content-Range || Request || &amp;lt;code&amp;gt;bytes START-END/TOTAL&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;bytes */TOTAL&amp;lt;/code&amp;gt; nur als Statusabfrage&lt;br /&gt;
|-&lt;br /&gt;
| Upload-Metadata || Request || tus-Metadaten im ersten Chunk: &amp;lt;code&amp;gt;filename&amp;lt;/code&amp;gt; (Pflicht), &amp;lt;code&amp;gt;filetype&amp;lt;/code&amp;gt; (optional), Base64-kodiert&lt;br /&gt;
|-&lt;br /&gt;
| Upload-Id || Request/Response || Session-Kennung. Fehlt sie im ersten Request, erzeugt der Server eine und gibt sie zurück; alle Folge-Chunks müssen sie mitsenden.&lt;br /&gt;
|-&lt;br /&gt;
| Upload-Offset || Response || Anzahl der bereits gespeicherten Bytes (= Startoffset des nächsten Chunks)&lt;br /&gt;
|-&lt;br /&gt;
| Range || Response || Bereits gespeicherter Bereich (&amp;lt;code&amp;gt;bytes=0-N&amp;lt;/code&amp;gt;)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Der Server hängt einen Chunk &#039;&#039;&#039;nur an, wenn START dem bereits gespeicherten Stand&lt;br /&gt;
entspricht&#039;&#039;&#039;. Bei Abweichung (doppelter Chunk, Lücke) wird nicht angehängt,&lt;br /&gt;
sondern der aktuelle &#039;&#039;Upload-Offset&#039;&#039; zurückgemeldet - der Client setzt ab dort&lt;br /&gt;
neu auf. Eine separate Statusabfrage ist für ein Resume daher nicht zwingend nötig.&lt;br /&gt;
&lt;br /&gt;
Überschreitet ein Chunk bzw. die Gesamtgrösse das Limit (&#039;&#039;re_upload_size&#039;&#039;),&lt;br /&gt;
antwortet der Server mit &#039;&#039;&#039;413 Payload Too Large&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Upload-Status liegt prozesslokal im Speicher. Nach einem Neustart des Servers muss ein unvollständiger Upload neu begonnen werden.}}&lt;br /&gt;
&lt;br /&gt;
== Komprimierte Uploads ==&lt;br /&gt;
&lt;br /&gt;
Ein Upload-Körper mit dem Header &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; wird mit &#039;&#039;&#039;415 Unsupported Media Type&#039;&#039;&#039; abgewiesen - bei beiden Verfahren, einfach wie chunked.&lt;br /&gt;
&lt;br /&gt;
Der Grund ist nicht Bequemlichkeit. Bei einem chunked Upload bliebe offen, ob &amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt; die gepackten oder die rohen Bytes zählt, und die SHA256-Prüfsumme bezöge sich nach einem Entpacken auf eine andere Datei, als der Client gesendet hat. Etablierte Protokolle - tus, S3 Multipart, Google Resumable Upload, Azure Blob - behandeln Kompression aus demselben Grund als Eigenschaft der &#039;&#039;&#039;gespeicherten Datei&#039;&#039;&#039; und nicht der Übertragung.&lt;br /&gt;
&lt;br /&gt;
Wer gepackt hochladen will, packt die Datei &#039;&#039;&#039;selbst&#039;&#039;&#039; und lädt sie als &amp;lt;code&amp;gt;export.csv.gz&amp;lt;/code&amp;gt; hoch. Offsets, Prüfsumme und Wiederaufnahme stimmen dann, und das Endpunkt-Skript bekommt Name und Typ wie gewohnt über &amp;lt;code&amp;gt;oReader.UploadName()&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;oReader.UploadType()&amp;lt;/code&amp;gt;.&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zugaenge&amp;diff=65546</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Zugaenge</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Zugaenge&amp;diff=65546"/>
		<updated>2026-09-25T09:28:33Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Fehlercode AUTH_FAILED und Obergrenze der Refresh-Laufzeit ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Zugänge=&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Zugang&#039;&#039;&#039; (Account) ist der Kanal, über den ein externer Konsument den REST-Server anspricht. Er bündelt API-Key, Host-Beschränkung, CORS-Konfiguration, optionale JWT-Authentifizierung und optionale mTLS-Subject-Prüfung.&lt;br /&gt;
&lt;br /&gt;
==Aufruf==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Zugänge&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Felder==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld          !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Name          || Sprechender Name, erscheint im Protokoll und der Statistik&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv         || Inaktive Zugänge werden bei der Authentifizierung mit 403 abgewiesen&lt;br /&gt;
|-&lt;br /&gt;
| API-Key       || Eindeutiger Schlüssel, den der Konsument im HTTP-Header &#039;&#039;apikey&#039;&#039; senden muss. Per Schlüsselsymbol generierbar&lt;br /&gt;
|-&lt;br /&gt;
| Host          || Optionaler Hostname oder IP-Adresse des Konsumenten. Stimmt die anfragende IP nicht überein, wird mit 403 abgelehnt&lt;br /&gt;
|-&lt;br /&gt;
| CORS-Origins  || Liste erlaubter Origins für Browser-Aufrufe, mehrere durch &#039;&#039;;&#039;&#039; getrennt. &#039;&#039;&#039;Leer heisst nicht „keine Einschränkung&amp;quot;, sondern „kein Browser-Zugriff&amp;quot;&#039;&#039;&#039; - siehe unten&lt;br /&gt;
|-&lt;br /&gt;
| mTLS-Subject  || Erwartete RDN-Komponente(n) im Client-Zertifikat-Subject, Komma-/Semikolon-getrennt (alle müssen vorkommen; nur sinnvoll bei mTLS-Servern)&lt;br /&gt;
|-&lt;br /&gt;
| JWT           || Schalter, ob für diesen Zugang ein JWT-Token nötig ist&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Endpunkt  || Pfad, über den &#039;&#039;&#039;alle Token-Vorgänge&#039;&#039;&#039; laufen: Anmelden, Erneuern und Abmelden (z.B. &#039;&#039;oauth&#039;&#039; oder &#039;&#039;meineapp/dev/auth&#039;&#039;). Verglichen wird der &#039;&#039;&#039;vollständige Pfad&#039;&#039;&#039;, mehrstufige Angaben mit &#039;&#039;/&#039;&#039; sind also erlaubt&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Key       || Geheimer Schlüssel zur Signierung/Verifikation, per Schlüsselsymbol generierbar&lt;br /&gt;
|-&lt;br /&gt;
| JWT-Exp       || Gültigkeitsdauer eines ausgestellten Access-Tokens in &#039;&#039;&#039;Minuten&#039;&#039;&#039;. Derselbe Wert wird dem Konsumenten in der Token-Antwort als &#039;&#039;expiresIn&#039;&#039; in &#039;&#039;&#039;Sekunden&#039;&#039;&#039; mitgeteilt und steht dem Endpunkt-Skript als &#039;&#039;_OBS_JWT_EXP_SEC&#039;&#039; zur Verfügung&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==API-Key==&lt;br /&gt;
&lt;br /&gt;
Der API-Key ist die Basis-Authentifizierung. Der Client übergibt ihn im HTTP-Header:&lt;br /&gt;
&lt;br /&gt;
 apikey: abcd-efgh-ijkl-mnop-qrstuvwxyz12&lt;br /&gt;
&lt;br /&gt;
Wird ein Aufruf ohne &#039;&#039;apikey&#039;&#039;-Header gestellt, sucht der Server intern nach einem Zugang mit API-Key &#039;&#039;&#039;*&#039;&#039;&#039; - damit lassen sich (mit Vorsicht!) auch öffentliche Endpunkte ohne Authentifizierung realisieren.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein Zugang mit API-Key &#039;&#039;*&#039;&#039; sollte nur für bewusst freigegebene Endpunkte verwendet werden und immer in Kombination mit einer Host-Beschränkung oder CORS-Beschränkung betrieben werden.}}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Nur für wirklich öffentliche Dateien.&#039;&#039;&#039; Ohne API-Key kennt der&lt;br /&gt;
Endpunkt weder Mandant noch Rolle - es gibt nichts, woran er eine Berechtigung&lt;br /&gt;
festmachen könnte ausser dem, was in der URL steht. Ein Wildcard-Zugang eignet&lt;br /&gt;
sich deshalb für Artefakte, die für jeden bestimmt sind (Test-APK, Handbuch),&lt;br /&gt;
&#039;&#039;&#039;nicht&#039;&#039;&#039; für Kundenbelege. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]].}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Bei einem Wildcard-Zugang sollte &#039;&#039;CORS-Origin&#039;&#039; &#039;&#039;&#039;leer&#039;&#039;&#039; bleiben.&lt;br /&gt;
Ein &#039;&#039;*&#039;&#039; dort wirkt auf die Preflight-Antwort &#039;&#039;&#039;aller&#039;&#039;&#039; Zugänge (siehe unten);&lt;br /&gt;
ein Download über die Adresszeile des Browsers sendet ohnehin kein &#039;&#039;Origin&#039;&#039;,&lt;br /&gt;
CORS greift dort also gar nicht.}}&lt;br /&gt;
&lt;br /&gt;
==Host-Beschränkung==&lt;br /&gt;
&lt;br /&gt;
Ist im Feld &#039;&#039;&#039;Host&#039;&#039;&#039; ein Wert hinterlegt, akzeptiert der Server Anfragen unter diesem Zugang nur dann, wenn die Remote-IP exakt diesem Host entspricht oder durch DNS-Auflösung auf diese IP zeigt. DNS-Auflösungen werden 5 Minuten gecached.&lt;br /&gt;
&lt;br /&gt;
==CORS==&lt;br /&gt;
&lt;br /&gt;
Soll ein Endpunkt aus einer Browser-Anwendung (Single-Page-App, JavaScript) heraus aufgerufen werden, muss das Origin der Anwendung in der CORS-Liste des Zugangs eingetragen sein.&lt;br /&gt;
&lt;br /&gt;
Format der Liste:&lt;br /&gt;
&lt;br /&gt;
 https://app.kunde.de;https://test.kunde.de;https://localhost:8080&lt;br /&gt;
&lt;br /&gt;
Ablauf:&lt;br /&gt;
&lt;br /&gt;
# Browser sendet einen Preflight-Request (&#039;&#039;OPTIONS&#039;&#039;). Der REST-Server prüft das Origin gegen die globale Origin-Liste &#039;&#039;&#039;aller&#039;&#039;&#039; Accounts (Cache, TTL 60s) und antwortet mit 204 + CORS-Header, falls erlaubt.&lt;br /&gt;
# Der eigentliche Request wird gegen die Origin-Liste &#039;&#039;&#039;des aktuell verwendeten Zugangs&#039;&#039;&#039; geprüft.&lt;br /&gt;
# Stimmt das Origin nicht, wird mit 403 &#039;&#039;Origin nicht erlaubt für Account ...&#039;&#039; abgelehnt.&lt;br /&gt;
&lt;br /&gt;
Der Preflight kann nicht zugangsbezogen prüfen: der Browser schickt dabei keinen&lt;br /&gt;
&#039;&#039;apikey&#039;&#039; mit, der Zugang ist zu diesem Zeitpunkt unbekannt. &#039;&#039;&#039;Die Durchsetzung&lt;br /&gt;
liegt deshalb auf Schritt 2&#039;&#039;&#039;, nicht auf dem Preflight. Praktische Folge: trägt&lt;br /&gt;
&#039;&#039;&#039;ein&#039;&#039;&#039; Zugang auf dem Server ein &#039;&#039;*&#039;&#039; in seiner Liste, antwortet jeder&lt;br /&gt;
Preflight mit &#039;&#039;Access-Control-Allow-Origin: *&#039;&#039; - auch für Zugänge mit engerer&lt;br /&gt;
Liste. Deren eigentliche Anfrage wird trotzdem gegen die eigene Liste geprüft.&lt;br /&gt;
&lt;br /&gt;
Erlaubte Methoden: &#039;&#039;GET, POST, PUT, DELETE, PATCH&#039;&#039; (systemseitig festgelegt).&lt;br /&gt;
&lt;br /&gt;
Bei den &#039;&#039;&#039;Anfrage-Headern&#039;&#039;&#039; spiegelt der Server im Preflight zurück, was der&lt;br /&gt;
Browser in &#039;&#039;Access-Control-Request-Headers&#039;&#039; angefragt hat. Damit sind eigene&lt;br /&gt;
Header eines Projekts - &#039;&#039;Idempotency-Key&#039;&#039;, &#039;&#039;If-Match&#039;&#039;, später&lt;br /&gt;
&#039;&#039;Upload-Offset&#039;&#039;/&#039;&#039;Upload-Length&#039;&#039; - ohne Änderung am Server nutzbar. Fragt der&lt;br /&gt;
Preflight keine Header an, antwortet der Server mit der Liste&lt;br /&gt;
&#039;&#039;Content-Type, apikey, Authorization, Accept&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Die Header-Liste ist &#039;&#039;&#039;keine Schutzgrenze&#039;&#039;&#039; - das ist das Origin. Sie&lt;br /&gt;
sagt dem Browser nur, welche Header der Server versteht. Dazu kommt, dass die&lt;br /&gt;
Zugangsdaten dieser Schnittstelle in Headern stehen (&#039;&#039;apikey&#039;&#039;,&lt;br /&gt;
&#039;&#039;Authorization&#039;&#039;) und nicht in Cookies: es gibt also keine automatisch&lt;br /&gt;
mitgeschickte Berechtigung, die eine fremde Seite ausnutzen könnte.}}&lt;br /&gt;
&lt;br /&gt;
===Was „leer&amp;quot; bedeutet===&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Ein &#039;&#039;&#039;leeres&#039;&#039;&#039; Feld CORS-Origins heisst &#039;&#039;&#039;nicht&#039;&#039;&#039; „keine&lt;br /&gt;
Einschränkung&amp;quot;. Es heisst: &#039;&#039;&#039;jeder Aufruf mit &#039;&#039;Origin&#039;&#039;-Header wird mit 403&lt;br /&gt;
abgelehnt&#039;&#039;&#039;. Für Clients &#039;&#039;&#039;ohne&#039;&#039;&#039; &#039;&#039;Origin&#039;&#039;-Header - mobile Apps, Server,&lt;br /&gt;
curl - ändert das Feld gar nichts, sie werden nie gegen CORS geprüft.}}&lt;br /&gt;
&lt;br /&gt;
Daraus ergeben sich drei sinnvolle Einstellungen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| leer || Kein Browser-Zugriff. Richtig für Zugänge, die nur von Apps oder Servern benutzt werden&lt;br /&gt;
|-&lt;br /&gt;
| konkrete Origins || Browser-Zugriff nur von diesen Seiten. Der Regelfall für Web-Anwendungen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;*&#039;&#039; || Browser-Zugriff von überall. Wirkt zusätzlich auf die Preflight-Antwort aller anderen Zugänge - bewusst und sparsam verwenden&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Für eine Browser-Anwendung ist dieses Feld besonders wichtig, weil ihr API-Key&lt;br /&gt;
zwangsläufig im JavaScript liegt und damit öffentlich ist. Die Origin-Liste&lt;br /&gt;
verhindert dann, dass jemand den Key nimmt und von seiner eigenen Seite benutzt.&lt;br /&gt;
Sie ist allerdings kein Ersatz für eine Zugriffskontrolle: wer den Key hat,&lt;br /&gt;
umgeht CORS mit einem einzigen Aufruf ausserhalb des Browsers.&lt;br /&gt;
&lt;br /&gt;
==JWT-Authentifizierung==&lt;br /&gt;
&lt;br /&gt;
Wird ein Zugang mit aktiver JWT-Pflicht aufgerufen, sind die folgenden Phasen zu durchlaufen (Phase 3 nur bei genutztem Refresh).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Anmelden, Erneuern und Abmelden laufen über denselben Endpunkt&#039;&#039;&#039; - den im&lt;br /&gt;
Feld JWT-Endpunkt hinterlegten Pfad. Ein eigener Endpunkt fürs Abmelden ist&lt;br /&gt;
nicht nötig und wäre auch umständlicher: der JWT-Endpunkt greift &#039;&#039;&#039;vor&#039;&#039;&#039; der&lt;br /&gt;
Adressauflösung und braucht deshalb weder eine Zeile in &#039;&#039;RESTSRV_ENDPOINTS&#039;&#039;&lt;br /&gt;
noch eine Berechtigung in &#039;&#039;RESTSRV_ACCESS&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Unterschieden wird nach Verb und Token-Art:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Ablauf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; ohne &#039;&#039;Authorization&#039;&#039; || Anmelden (Skript-Methode &#039;&#039;Authenticate&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; mit Bearer, Refresh-Token || Erneuern (Skript-Methode &#039;&#039;Refresh&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;DELETE&#039;&#039; mit Bearer, Access-Token || Abmelden (kein Skript nötig)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;POST&#039;&#039; mit Bearer, Access-Token || &#039;&#039;&#039;401&#039;&#039;&#039; - unverändert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Dass das Abmelden am Verb hängt und nicht allein an der Token-Art, ist&lt;br /&gt;
Absicht. Würde ein vorgelegtes Access-Token beim &#039;&#039;POST&#039;&#039; als Abmeldung gedeutet,&lt;br /&gt;
würde ein Client, der seine beiden Token verwechselt, den Anwender still&lt;br /&gt;
abmelden - statt das klare 401 zu bekommen, an dem der Fehler auffällt.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 1: Token-Bezug===&lt;br /&gt;
&lt;br /&gt;
Der Konsument ruft den JWT-Endpunkt des Zugangs auf, z.B.:&lt;br /&gt;
&lt;br /&gt;
 POST https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
 Body:   {&amp;quot;username&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;password&amp;quot;:&amp;quot;...&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Der Server führt das im Zugang hinterlegte &#039;&#039;&#039;Authentifizierungs-Skript&#039;&#039;&#039; aus (F7 in der Zugangs-Liste). Das Skript muss eine Prozedur &#039;&#039;Authenticate&#039;&#039; bereitstellen, die über den Writer schreibt:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;status&#039;&#039; = &#039;&#039;1&#039;&#039; bei Erfolg, &#039;&#039;9&#039;&#039; bei Misserfolg&lt;br /&gt;
* &#039;&#039;error&#039;&#039; = Fehlertext (bei status=9)&lt;br /&gt;
* &#039;&#039;_OBS_JWT_ID&#039;&#039;, &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039;, &#039;&#039;_OBS_JWT_AUDIENCE&#039;&#039; für die JWT-Claims (optional)&lt;br /&gt;
* &#039;&#039;_OBS_JWT_CLAIM_&amp;amp;lt;name&amp;amp;gt;&#039;&#039; für beliebige Custom-Claims (z.B. Mandant &#039;&#039;tenant&#039;&#039;, Rollen &#039;&#039;roles&#039;&#039;); im Folge-Skript lesbar&lt;br /&gt;
* &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039; (optional) löst die Ausstellung eines Refresh-Tokens aus; &#039;&#039;_OBS_JWT_REFRESH_EXP&#039;&#039; setzt dessen Lebensdauer in Minuten (Default 90 Tage)&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Die Lebensdauer eines Refresh-Tokens ist nach oben auf &#039;&#039;&#039;372 Tage&#039;&#039;&#039;&lt;br /&gt;
begrenzt. Ein mit &#039;&#039;_OBS_JWT_REFRESH_EXP&#039;&#039; länger ausgestelltes Token wird bei der&lt;br /&gt;
Vorlage &#039;&#039;&#039;nicht mehr verifiziert&#039;&#039;&#039; und mit 401 abgewiesen - der Fehler fällt&lt;br /&gt;
also erst Monate später auf, beim ersten Erneuerungsversuch.}}&lt;br /&gt;
&lt;br /&gt;
Die Signatur lautet &#039;&#039;procedure Authenticate(oReader: TxRestReader; oWriter: TxRestWriter);&#039;&#039; - Details unter [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]]. Bei Erfolg signiert der Server einen Token (HS256, Issuer &#039;&#039;OBS REST-Server&#039;&#039;, Lebenszeit gemäß JWT-Exp) und antwortet:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;token&amp;quot;:        &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;accessToken&amp;quot;:  &amp;quot;eyJhbGciOi...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;:    28800,&lt;br /&gt;
  &amp;quot;serverTime&amp;quot;:   &amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Zum Aufbau der Antwort:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;token&#039;&#039; und &#039;&#039;accessToken&#039;&#039; enthalten &#039;&#039;&#039;denselben&#039;&#039;&#039; Token. &#039;&#039;accessToken&#039;&#039; ist der Name, den die meisten Client-Bibliotheken erwarten; &#039;&#039;token&#039;&#039; bleibt für bestehende Konsumenten erhalten.&lt;br /&gt;
* &#039;&#039;expiresIn&#039;&#039; ist die Gültigkeit in &#039;&#039;&#039;Sekunden&#039;&#039;&#039; (JWT-Exp × 60).&lt;br /&gt;
* &#039;&#039;&#039;Alle weiteren Felder, die das Authenticate-Skript zurückgibt, werden mit ausgeliefert&#039;&#039;&#039; - mit Ausnahme von &#039;&#039;status&#039;&#039; und den &#039;&#039;_OBS_&#039;&#039;-Steuerfeldern. So kann das Skript z.B. eine Benutzer-Id, den Mandanten oder Rollen direkt in die Anmelde-Antwort legen, ohne dass der Client dafür einen zweiten Aufruf braucht.&lt;br /&gt;
&lt;br /&gt;
Beispiel mit Zusatzfeldern aus dem Skript:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;benutzerId&amp;quot;: &amp;quot;4711&amp;quot;, &amp;quot;tenant&amp;quot;: &amp;quot;nord&amp;quot;, &amp;quot;roles&amp;quot;: [&amp;quot;TECHNIKER&amp;quot;],&lt;br /&gt;
  &amp;quot;token&amp;quot;: &amp;quot;eyJ...&amp;quot;, &amp;quot;accessToken&amp;quot;: &amp;quot;eyJ...&amp;quot;, &amp;quot;refreshToken&amp;quot;: &amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;: 28800, &amp;quot;serverTime&amp;quot;: &amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
Mit der Ausstellung legt der Server zu dem Token-Paar eine &#039;&#039;&#039;Sitzungszeile&#039;&#039;&#039; in&lt;br /&gt;
&#039;&#039;RESTSRV_TOKEN&#039;&#039; an. Lässt sich diese Zeile nicht schreiben (Tabelle fehlt,&lt;br /&gt;
Datenbankproblem), wird &#039;&#039;&#039;kein Token ausgeliefert&#039;&#039;&#039;: der Server antwortet mit&lt;br /&gt;
&#039;&#039;&#039;503&#039;&#039;&#039; und &#039;&#039;Retry-After&#039;&#039;. Das ist bewusst von der abgelehnten Anmeldung&lt;br /&gt;
unterschieden - die Zugangsdaten waren in Ordnung, der Versuch darf wiederholt&lt;br /&gt;
werden.&lt;br /&gt;
&lt;br /&gt;
Wird die Anmeldung abgelehnt (&#039;&#039;status&#039;&#039; = 9), antwortet der Server mit&lt;br /&gt;
&#039;&#039;&#039;401 Unauthorized&#039;&#039;&#039;, dem Code &#039;&#039;AUTH_FAILED&#039;&#039; und reicht den Text aus dem Feld&lt;br /&gt;
&#039;&#039;error&#039;&#039; als &#039;&#039;error.message&#039;&#039; an den Client durch - der Konsument kann ihn also&lt;br /&gt;
direkt anzeigen. Die Formulierung liegt damit beim Skript.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;AUTH_FAILED&#039;&#039; unterscheidet die &#039;&#039;&#039;abgelehnte Anmeldung&#039;&#039;&#039; vom&lt;br /&gt;
&#039;&#039;AUTH_EXPIRED&#039;&#039;, das ein abgelaufenes oder gesperrtes Token liefert. Ein Client&lt;br /&gt;
sollte auf den Code reagieren und nicht auf den Text: Bei &#039;&#039;AUTH_FAILED&#039;&#039; gehört&lt;br /&gt;
die Anmeldemaske noch einmal vorgelegt, bei &#039;&#039;AUTH_EXPIRED&#039;&#039; erst ein Refresh&lt;br /&gt;
versucht.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der durchgereichte Text ist für den Endanwender gedacht und sollte&lt;br /&gt;
keine internen Details verraten. „Benutzer oder Passwort ist falsch&amp;quot; ist eine gute&lt;br /&gt;
Meldung, „Kein Datensatz in BENUTZ mit b_name=meier&amp;quot; nicht.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 2: Token-Verwendung===&lt;br /&gt;
&lt;br /&gt;
Bei allen folgenden Aufrufen sendet der Client den Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 GET https://api.meinserver.de/kalender/v1&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi...&lt;br /&gt;
&lt;br /&gt;
Der Server verifiziert Signatur, Issuer, Ausstellzeitpunkt und Ablauf (Toleranz: 20 Sekunden Clock-Skew) und prüft anschliessend die &#039;&#039;&#039;Sitzung&#039;&#039;&#039; in &#039;&#039;RESTSRV_TOKEN&#039;&#039;. Im Skript stehen die Claims als Parameter &#039;&#039;_OBS_JWT_ID&#039;&#039;, &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039; und &#039;&#039;_OBS_JWT_AUDIENCE&#039;&#039; zur Verfügung.&lt;br /&gt;
&lt;br /&gt;
Die Sitzungsprüfung weist ein Token ab, das zu keiner Zeile gehört oder dessen&lt;br /&gt;
Sitzung gesperrt wurde - beides mit &#039;&#039;&#039;401&#039;&#039;&#039; und dem Code &#039;&#039;AUTH_EXPIRED&#039;&#039;. Der&lt;br /&gt;
Grund steht jeweils im Protokoll &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; unter &#039;&#039;TokenStore: ...&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein Token, das &#039;&#039;&#039;vor&#039;&#039;&#039; der Einführung dieser Prüfung ausgestellt&lt;br /&gt;
wurde, gehört zu keiner Sitzung und wird abgewiesen. Nach dem Update müssen sich&lt;br /&gt;
angemeldete Konsumenten also einmalig neu anmelden.}}&lt;br /&gt;
&lt;br /&gt;
===Phase 3: Token erneuern (Refresh)===&lt;br /&gt;
&lt;br /&gt;
Soll die App lange ohne Neuanmeldung arbeiten, stellt das Authenticate-Skript zusätzlich ein Refresh-Token aus (Feld &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039;). Zum Erneuern ruft der Client &#039;&#039;&#039;denselben JWT-Endpunkt&#039;&#039;&#039; auf, aber mit dem Refresh-Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 POST https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi... (Refresh-Token)&lt;br /&gt;
&lt;br /&gt;
Der Server erkennt am Bearer-Token den Refresh-Fall, verifiziert das Token (Signatur, Ablauf, &#039;&#039;token_use=refresh&#039;&#039;), &#039;&#039;&#039;entwertet es in der Sitzung&#039;&#039;&#039; und ruft erst dann im selben Skript die Methode &#039;&#039;&#039;Refresh&#039;&#039;&#039; auf. Das Skript liefert nur noch die aktuellen Claims zurück - Rotation, Einmalgebrauch und Sperrliste erledigt der Server. Die Antwort ist ein rotiertes Paar &#039;&#039;{&amp;quot;token&amp;quot;,&amp;quot;accessToken&amp;quot;,&amp;quot;refreshToken&amp;quot;,&amp;quot;expiresIn&amp;quot;,&amp;quot;serverTime&amp;quot;}&#039;&#039; in &#039;&#039;&#039;derselben Sitzung&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Eine &#039;&#039;&#039;eigene Sperrtabelle im Skript ist nicht mehr nötig&#039;&#039;&#039; und sollte&lt;br /&gt;
entfernt werden. Wer sie weiterführt, rotiert zweimal: einmal im Skript, einmal im&lt;br /&gt;
Server. Der Server sieht die Skript-Tabelle nicht, und das Skript sieht die&lt;br /&gt;
Sitzung nicht.}}&lt;br /&gt;
&lt;br /&gt;
Verhalten bei einem bereits benutzten Refresh-Token:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Lage !! Antwort !! Wirkung auf die Sitzung&lt;br /&gt;
|-&lt;br /&gt;
| Erneute Vorlage &#039;&#039;&#039;innerhalb von 60 Sekunden&#039;&#039;&#039; || 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; || Keine. Das ist der Normalfall einer App, deren parallele Anfragen gleichzeitig in den Refresh laufen - eine gewinnt, die anderen sollen die vom Gewinner gespeicherten Token benutzen&lt;br /&gt;
|-&lt;br /&gt;
| Erneute Vorlage &#039;&#039;&#039;später&#039;&#039;&#039; || 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; || Die &#039;&#039;&#039;gesamte Sitzung wird gesperrt&#039;&#039;&#039;. Das Token wurde kopiert oder wiederholt, ohne dass ein Gewinner es noch brauchen könnte; der Vorfall wird mit IP protokolliert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Details und ein vollständiges Beispiel siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
===Phase 4: Abmelden (Logout)===&lt;br /&gt;
&lt;br /&gt;
Abgemeldet wird mit einem &#039;&#039;&#039;DELETE&#039;&#039;&#039; auf denselben JWT-Endpunkt, das&lt;br /&gt;
Access-Token im &#039;&#039;Authorization&#039;&#039;-Header:&lt;br /&gt;
&lt;br /&gt;
 DELETE https://api.meinserver.de/oauth/&lt;br /&gt;
 Header: apikey: [API-KEY]&lt;br /&gt;
         Authorization: Bearer eyJhbGciOi... (Access-Token)&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 204 No Content&lt;br /&gt;
&lt;br /&gt;
Der Server sperrt die &#039;&#039;&#039;ganze Sitzung&#039;&#039;&#039; - also auch die noch nicht&lt;br /&gt;
abgelaufenen Access-Token aller vorherigen Erneuerungen. &#039;&#039;&#039;Ein Skript ist dafür&lt;br /&gt;
nicht nötig&#039;&#039;&#039;: es gibt keine Methode &#039;&#039;Logout&#039;&#039;, der Vorgang läuft komplett in&lt;br /&gt;
der Engine. Damit funktioniert das Abmelden für jeden Zugang mit aktiver&lt;br /&gt;
JWT-Pflicht, ohne dass am Authentifizierungs-Skript etwas geändert wird.&lt;br /&gt;
&lt;br /&gt;
* Ein wiederholter Aufruf ist unschädlich und bleibt &#039;&#039;&#039;204&#039;&#039;&#039; - auch wenn die Sitzung längst gesperrt ist.&lt;br /&gt;
* Ein abgelaufenes oder ungültiges Token ergibt &#039;&#039;&#039;401&#039;&#039;&#039; (es wird schon bei der Prüfung abgewiesen).&lt;br /&gt;
* Ein Refresh-Token statt des Access-Tokens ergibt &#039;&#039;&#039;401&#039;&#039;&#039;.&lt;br /&gt;
* Lässt sich die Sitzung nicht sperren, antwortet der Server &#039;&#039;&#039;503&#039;&#039;&#039; mit &#039;&#039;Retry-After&#039;&#039; - ein Client darf nicht glauben, er sei abgemeldet, während sein Token weiterläuft.&lt;br /&gt;
&lt;br /&gt;
====Alle Geräte abmelden====&lt;br /&gt;
&lt;br /&gt;
Der Fall „Gerät verloren&amp;quot; ist bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; am JWT-Endpunkt untergebracht:&lt;br /&gt;
er braucht eine fachliche Entscheidung darüber, wer das darf. Dafür gibt es das&lt;br /&gt;
&#039;&#039;oWriter.JwtRevoke&#039;&#039;, das jeder gewöhnliche Endpunkt aufrufen kann:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    // &#039;session&#039; = nur diese Sitzung, &#039;all&#039; = alle Sitzungen des Subjects&lt;br /&gt;
    oWriter.JwtRevoke(&#039;all&#039;);&lt;br /&gt;
    oWriter.Status(204);&lt;br /&gt;
    oWriter.NoBody();&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;all&#039;&#039; setzt voraus, dass das Authenticate-Skript &#039;&#039;_OBS_JWT_SUBJECT&#039;&#039;&lt;br /&gt;
gesetzt hat. Ohne Subject lässt sich die Menge der Sitzungen nicht bestimmen, und&lt;br /&gt;
der Aufruf antwortet mit 503.}}&lt;br /&gt;
&lt;br /&gt;
==Woher ein Anmelder seine Identität bekommt==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Diese Frage entscheidet das Skript des Zugangs, nicht der Server.&#039;&#039;&#039; Das&lt;br /&gt;
Authentifizierungs-Skript liegt je Zugang in &amp;lt;code&amp;gt;ra_jwt_script&amp;lt;/code&amp;gt; - und&lt;br /&gt;
damit gilt für jede Anwendung ihre eigene Regel.&lt;br /&gt;
&lt;br /&gt;
Das ist keine Kleinigkeit, sondern der Punkt, an dem sich zwei Anwendungen&lt;br /&gt;
unterscheiden. Ein Beispiel aus dem Betrieb: Ein Zugang für eine&lt;br /&gt;
Techniker-Anwendung lehnt jeden Benutzer ohne Mitarbeiterzuordnung&lt;br /&gt;
(&amp;lt;code&amp;gt;BENUTZ.b_mitarbeiter&amp;lt;/code&amp;gt;) ab - &#039;&#039;&#039;bewusst&#039;&#039;&#039;, denn ein Techniker ohne&lt;br /&gt;
Zuordnung bekäme eine leere Auftragsliste und würde den Fehler bei sich suchen.&lt;br /&gt;
Für einen Kundenzugang wäre genau dieselbe Regel falsch: sie träfe &#039;&#039;&#039;jeden&#039;&#039;&#039;&lt;br /&gt;
Anmelder.&lt;br /&gt;
&lt;br /&gt;
Wer einen zweiten Zugang für eine andere Anwendung anlegt, prüft deshalb als&lt;br /&gt;
Erstes: Woraus besteht die Identität dieser Anmelder, und welche Ablehnung ist&lt;br /&gt;
für sie richtig? Ein kopiertes Skript bringt die Regel der anderen Anwendung&lt;br /&gt;
mit, und sie fällt erst auf, wenn sich der erste echte Anmelder nicht anmelden&lt;br /&gt;
kann.&lt;br /&gt;
&lt;br /&gt;
==Sitzungen (RESTSRV_TOKEN)==&lt;br /&gt;
&lt;br /&gt;
Je ausgestelltem Token-Paar entsteht eine Zeile. Sie enthält die Sitzung, die&lt;br /&gt;
Zeilenkennung, das Subject, die beiden Laufzeiten, IP und Trace-ID der Ausstellung&lt;br /&gt;
sowie die Kennzeichen „verbraucht&amp;quot; und „gesperrt&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Rotation und Widerruf sind getrennt.&#039;&#039;&#039; Ein Refresh entwertet nur das Refresh-Token; der zugehörige Access-Token bleibt bis zu seinem Ablauf gültig. Sonst bekämen parallel laufende Anfragen ein 401, obwohl niemand abgemeldet ist.&lt;br /&gt;
* &#039;&#039;&#039;Die Sitzung überlebt die Rotation.&#039;&#039;&#039; Alle Paare einer Anmeldung tragen dieselbe Sitzungskennung - deshalb kann ein Logout sie gemeinsam sperren.&lt;br /&gt;
* &#039;&#039;&#039;Aufräumen läuft automatisch&#039;&#039;&#039; beim Serverstart und danach höchstens stündlich. Gelöscht wird nur, was nichts mehr entscheiden kann: Zeilen, deren Access- &#039;&#039;&#039;und&#039;&#039;&#039; Refresh-Laufzeit vorbei sind. Eine verbrauchte Zeile bleibt bis dahin stehen, weil sonst die Erkennung eines später vorgelegten gestohlenen Refresh-Tokens verloren ginge.&lt;br /&gt;
* &#039;&#039;&#039;Kein Eingriff nötig.&#039;&#039;&#039; Die Tabelle wird nicht gepflegt; sie ist Diagnose-Material. Alle Abweisungen stehen als &#039;&#039;TokenStore: ...&#039;&#039; in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==mTLS-Subject-Prüfung==&lt;br /&gt;
&lt;br /&gt;
Wird im Feld &#039;&#039;&#039;mTLS-Subject&#039;&#039;&#039; ein Wert hinterlegt, prüft der Server zusätzlich zur Standard-mTLS-Validierung, ob der Subject-DN des (CA-validierten) Client-Zertifikats die hinterlegten &#039;&#039;&#039;RDN-Komponenten&#039;&#039;&#039; enthält. Verglichen wird jede Komponente &#039;&#039;&#039;vollständig&#039;&#039;&#039; (kein Substring-Match) und case-insensitiv; mehrere Komponenten werden durch Komma oder Semikolon getrennt und müssen &#039;&#039;&#039;alle&#039;&#039;&#039; im Subject vorkommen.&lt;br /&gt;
&lt;br /&gt;
Beispiele für Subject-Strings im Cert (OneLine-DN):&lt;br /&gt;
&lt;br /&gt;
 /C=DE/O=Kunde GmbH/CN=client-prod.kunde.de&lt;br /&gt;
 /C=DE/O=Kunde GmbH/CN=client-test.kunde.de&lt;br /&gt;
&lt;br /&gt;
Mit &#039;&#039;&#039;CN=client-prod.kunde.de&#039;&#039;&#039; im Feld wird genau das prod-Cert akzeptiert, das test-Cert mit 401 abgewiesen. Da jede RDN-Komponente vollständig verglichen wird, matcht z.B. &#039;&#039;&#039;CN=client1&#039;&#039;&#039; nicht fälschlich auch &#039;&#039;&#039;CN=client10&#039;&#039;&#039;. Mehrere Bedingungen lassen sich mit Komma/Semikolon kombinieren, z.B. &#039;&#039;O=Kunde GmbH;CN=client-prod.kunde.de&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==Listen-Funktionen==&lt;br /&gt;
&lt;br /&gt;
In der Zugänge-Liste (allein):&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfg&#039;&#039;&#039; - neuer Zugang&lt;br /&gt;
* &#039;&#039;&#039;Return&#039;&#039;&#039; - Zugang bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F4&#039;&#039;&#039; - Sortierung&lt;br /&gt;
* &#039;&#039;&#039;F7&#039;&#039;&#039; - JWT-Authentifizierungs-Skript bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F8&#039;&#039;&#039; - Statistik für den Zugang&lt;br /&gt;
&lt;br /&gt;
Aus der Endpunkt-Liste via F6 geöffnet (Auswahl für Berechtigung):&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;F2&#039;&#039;&#039; - markierte Zugänge dem Endpunkt zuordnen&lt;br /&gt;
* &#039;&#039;&#039;F5&#039;&#039;&#039; - Zugang in der Liste markieren / Markierung aufheben&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=65545</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Server-Profile</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Server-Profile&amp;diff=65545"/>
		<updated>2026-09-25T09:28:33Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: HSTS-Header und Abgrenzung der Verbindungsgrenzen ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Server-Profile=&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Server&#039;&#039;&#039; im REST-Modul ist ein eigenständiges TLS-Profil mit einer eigenen HTTP-Server-Instanz. In OBS können beliebig viele Server-Profile parallel betrieben werden, jedes mit eigenem Modus, eigenen Zertifikaten und eigenen Bindungen.&lt;br /&gt;
&lt;br /&gt;
Typische Einsatzfälle für mehrere Profile:&lt;br /&gt;
&lt;br /&gt;
* Ein &#039;&#039;&#039;Public-Profil&#039;&#039;&#039; mit Standard-TLS auf Port 443 für externe Konsumenten.&lt;br /&gt;
* Ein &#039;&#039;&#039;Internal-mTLS-Profil&#039;&#039;&#039; auf einem internen Port für Microservices, die zwingend ein Client-Zertifikat vorlegen müssen.&lt;br /&gt;
* Ein &#039;&#039;&#039;Debug-Profil&#039;&#039;&#039; mit Plain-HTTP auf 127.0.0.1, ausschliesslich für lokale Tests.&lt;br /&gt;
&lt;br /&gt;
==Aufruf==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Server&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Felder des Server-Profils==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld                !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Nr                  || Eindeutige Nummer 1-99, einmalig vergeben, kann später nicht mehr geändert werden&lt;br /&gt;
|-&lt;br /&gt;
| Name                || Sprechender Name, eindeutig - erscheint in Protokoll, Statistik und Endpunkt-Zuordnung&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv               || Profil wird beim Start des REST-Dienstes geladen. Inaktive Profile starten nicht&lt;br /&gt;
|-&lt;br /&gt;
| Info                || Freitext zur Dokumentation (interner Kommentar, wird nicht ausgewertet)&lt;br /&gt;
|-&lt;br /&gt;
| Kein SSL            || Plain-HTTP-Modus, ausschliesslich für Debug. TLS-Felder werden ignoriert&lt;br /&gt;
|-&lt;br /&gt;
| mTLS                || Mutual TLS: jeder Client muss ein gültiges Client-Zertifikat vorlegen&lt;br /&gt;
|-&lt;br /&gt;
| Cert                || PEM-Inhalt des Server-Zertifikats (inkl. ggf. Zwischen-CA-Kette)&lt;br /&gt;
|-&lt;br /&gt;
| Key                 || PEM-Inhalt des privaten Schlüssels&lt;br /&gt;
|-&lt;br /&gt;
| Root-Cert           || PEM-Inhalt des CA-Root-Zertifikats (Pflicht bei Standard-TLS, dort für Chain-Validierung, sowie bei mTLS, dort zusätzlich für Client-Cert-Verifikation)&lt;br /&gt;
|-&lt;br /&gt;
| Limit-Fenster       || Länge des Rate-Limit-Fensters in Sekunden. Leer/0 = Standard 60&lt;br /&gt;
|-&lt;br /&gt;
| Limit Erfolg        || Erlaubte erfolgreiche Anfragen je Schlüssel und Fenster. Leer/0 = Standard 120&lt;br /&gt;
|-&lt;br /&gt;
| Limit Fehler        || Erlaubte fehlgeschlagene Anfragen je Schlüssel und Fenster. Leer/0 = Standard 10&lt;br /&gt;
|-&lt;br /&gt;
| Limit gesamt        || Erlaubte Anfragen des gesamten Profils je Fenster. Leer/0 = Standard 600&lt;br /&gt;
|-&lt;br /&gt;
| Max. Verbindungen   || Obergrenze gleichzeitiger &#039;&#039;&#039;Verbindungen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_conn&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Sie deckt auch laufende Datei-Downloads ab, weil der Platz erst beim Trennen frei wird. Bei Überschreitung trennt der Server &#039;&#039;&#039;ohne HTTP-Antwort&#039;&#039;&#039; - der Wert gehört deutlich über den Normalbetrieb, als Reissleine&lt;br /&gt;
|-&lt;br /&gt;
| Max. parallel       || Obergrenze gleichzeitig &#039;&#039;&#039;verarbeiteter Anfragen&#039;&#039;&#039; (&amp;lt;code&amp;gt;rsv_max_parallel&amp;lt;/code&amp;gt;). Leer/0 = unbegrenzt. Bei Überschreitung antwortet der Server &#039;&#039;&#039;503&#039;&#039;&#039; mit &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;, der Client darf es also gleich erneut versuchen&lt;br /&gt;
|-&lt;br /&gt;
| Keine Kompression   || Antworten dieses Profils werden &#039;&#039;&#039;nicht&#039;&#039;&#039; gepackt (&amp;lt;code&amp;gt;rsv_no_gzip&amp;lt;/code&amp;gt;). Ohne Haken packt der Server JSON- und Textantworten ab 1024 Byte mit gzip, sofern der Client sie über &amp;lt;code&amp;gt;Accept-Encoding&amp;lt;/code&amp;gt; annimmt. Der Haken betrifft nur die Antwortrichtung - komprimierte &#039;&#039;&#039;Anfragen&#039;&#039;&#039; nimmt der Server weiterhin entgegen&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;Max. Verbindungen&#039;&#039; und &#039;&#039;Max. parallel&#039;&#039; sind die beiden&lt;br /&gt;
&#039;&#039;&#039;Lastgrenzen&#039;&#039;&#039; des Profils. Davon zu unterscheiden sind die festen Grenzen der&lt;br /&gt;
persistenten Verbindungen - 10 Sekunden Leerlauf, 100 Anfragen je Verbindung -,&lt;br /&gt;
die nicht der Last dienen, sondern der Hygiene und nicht einstellbar sind. Siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer|REST-Server]], Abschnitt Persistente&lt;br /&gt;
Verbindungen.}}&lt;br /&gt;
&lt;br /&gt;
Die Inhalte der Zertifikate werden direkt in OBS als Text gespeichert. Zur Laufzeit schreibt der Server die PEM-Inhalte für den Bruchteil einer Sekunde in ein Temp-Verzeichnis, damit OpenSSL sie beim Start lesen kann; danach werden die Temp-Dateien sofort wieder gelöscht.&lt;br /&gt;
&lt;br /&gt;
==Modi==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Modus               !! Voraussetzungen                       !! Anwendung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039;  || Cert + Key + Root-Cert hinterlegt     || Empfohlener Default für alle Server, die über das öffentliche Netz erreichbar sind&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;mTLS&#039;&#039;&#039;          || Cert + Key + Root-Cert (CA)           || Maschine-zu-Maschine-Kommunikation, in der jeder Client mit Cert validiert wird&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Plain-HTTP&#039;&#039;&#039;    || Haken &#039;&#039;Kein SSL&#039;&#039;                    || Ausschliesslich lokale Tests. Erkennt TLS-Versuche auf dem Plain-Port und trennt sie direkt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beide Richtungen werden erkannt: Eine &#039;&#039;&#039;unverschlüsselte Anfrage an einen&lt;br /&gt;
TLS-Port&#039;&#039;&#039; (&#039;&#039;http&#039;&#039; statt &#039;&#039;https&#039;&#039;) beantwortet der Server mit &#039;&#039;&#039;HTTP 400&#039;&#039;&#039;&lt;br /&gt;
und einem Klartext-Hinweis. Die Abweisung erfolgt &#039;&#039;&#039;vor&#039;&#039;&#039; Authentifizierung,&lt;br /&gt;
Rate-Limit und Endpunkt-Skript - ein falsch konfigurierter Client löst also&lt;br /&gt;
keine Verarbeitung aus.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Bei Standard-TLS und mTLS fährt der Server ohne Cert/Key gar nicht erst hoch. Bei Standard-TLS ohne Root-Cert wird die Anlage abgelehnt - die Chain-Validierung benötigt das CA-Root. Plain-HTTP umgeht TLS komplett und ist nicht für Produktivumgebungen geeignet.}}&lt;br /&gt;
&lt;br /&gt;
==TLS-Version, Cipher und Zertifikats-Rotation==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;TLS-Version:&#039;&#039;&#039; Der Server erzwingt &#039;&#039;&#039;TLS 1.2&#039;&#039;&#039;. Die Cipher-Liste ist auf ECDHE-Schlüsselaustausch (Forward Secrecy) mit AEAD-Verfahren (AES-GCM, ChaCha20-Poly1305) beschränkt; CBC, statisches RSA-KEX und DHE sind ausgeschlossen.&lt;br /&gt;
* &#039;&#039;&#039;HSTS:&#039;&#039;&#039; Jede Antwort eines TLS-Profils trägt &#039;&#039;Strict-Transport-Security: max-age=31536000; includeSubDomains&#039;&#039;. Ein Browser spricht die Adresse damit ein Jahr lang nur noch über HTTPS an. Beim Profil &#039;&#039;Kein SSL&#039;&#039; wird der Header bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; gesetzt - der Browser würde ihn sonst speichern und die Erzwingung auch auf produktive Sitzungen anwenden.&lt;br /&gt;
* &#039;&#039;&#039;Rotation:&#039;&#039;&#039; Ein Zertifikatswechsel erfolgt durch ändern der PEM-Inhalte in der DB und einen &#039;&#039;&#039;Neustart&#039;&#039;&#039; des REST-Dienstes (kein Hot-Reload). Bis zum Neustart läuft das alte Zertifikat weiter.&lt;br /&gt;
* &#039;&#039;&#039;Certificate-Pinning:&#039;&#039;&#039; Mobile Clients können den Server-Public-Key pinnen. Zertifikatswechsel daher &#039;&#039;&#039;rechtzeitig ankündigen&#039;&#039;&#039; und mit einem &#039;&#039;&#039;Backup-Pin&#039;&#039;&#039; vorbereiten - der neue Pin muss vor dem Wechsel in einer Client-Version ausgerollt sein, sonst sperrt sich die App aus.&lt;br /&gt;
==Rate-Limit je Profil==&lt;br /&gt;
&lt;br /&gt;
Jedes Server-Profil führt &#039;&#039;&#039;eigene Zähler&#039;&#039;&#039; und kann &#039;&#039;&#039;eigene Schwellen&#039;&#039;&#039; haben.&lt;br /&gt;
Ein Profil, das gerade überrannt wird, bremst damit die anderen Profile nicht mit&lt;br /&gt;
aus - das Public-Profil und das interne mTLS-Profil sind voneinander unabhängig.&lt;br /&gt;
&lt;br /&gt;
Gezählt wird pro Schlüssel, und der Schlüssel ist der &#039;&#039;&#039;API-Key&#039;&#039;&#039; des Zugangs.&lt;br /&gt;
Sendet ein Konsument keinen API-Key (Zugang mit &#039;&#039;*&#039;&#039;), wird ersatzweise auf die&lt;br /&gt;
&#039;&#039;&#039;IP-Adresse&#039;&#039;&#039; gezählt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Das ist der wichtigste Punkt bei mobilen Anwendungen: Sitzen alle&lt;br /&gt;
Benutzer hinter einer gemeinsamen Firmen-IP und sendet die App keinen eigenen&lt;br /&gt;
API-Key, teilen sich &#039;&#039;&#039;alle Benutzer einen Zähler&#039;&#039;&#039;. Mit den Standardwerten sind&lt;br /&gt;
das 10 Fehlversuche für den ganzen Betrieb pro Minute - nach ein paar falschen&lt;br /&gt;
Passworteingaben steht die Abteilung. Für solche Profile die Schwellen bewusst&lt;br /&gt;
höher setzen oder jedem Konsumenten einen eigenen API-Key geben.}}&lt;br /&gt;
&lt;br /&gt;
Bleiben alle vier Felder leer (bzw. 0), gelten die Standardwerte. Die Werte werden&lt;br /&gt;
&#039;&#039;&#039;beim Start&#039;&#039;&#039; des REST-Dienstes gelesen - eine Änderung wird erst nach einem&lt;br /&gt;
Neustart wirksam.&lt;br /&gt;
&lt;br /&gt;
==Validierung beim Speichern==&lt;br /&gt;
&lt;br /&gt;
Beim Speichern wird geprüft:&lt;br /&gt;
&lt;br /&gt;
* Nr und Name müssen vergeben sein.&lt;br /&gt;
* Nr muss eindeutig sein.&lt;br /&gt;
* Name muss eindeutig sein.&lt;br /&gt;
* Bei nicht aktiviertem &#039;&#039;Kein SSL&#039;&#039; müssen Cert und Key vorhanden sein.&lt;br /&gt;
* Bei nicht aktiviertem &#039;&#039;mTLS&#039;&#039; (= Standard-TLS) muss Root-Cert vorhanden sein.&lt;br /&gt;
&lt;br /&gt;
==Bindungen==&lt;br /&gt;
&lt;br /&gt;
Eine Bindung ist die Kombination aus IP-Adresse und Port, auf der ein Server-Profil lauscht. Pro Server-Profil können mehrere Bindungen existieren, z.B. um die gleiche TLS-Konfiguration parallel auf 443 und 8443 anzubieten.&lt;br /&gt;
&lt;br /&gt;
===Aufruf===&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste den gewünschten Server markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die zum Server gehörende Bindungs-Liste.&lt;br /&gt;
&lt;br /&gt;
===Felder===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld    !! Beschreibung&lt;br /&gt;
|-&lt;br /&gt;
| Host    || IP-Adresse oder Hostname, auf dem gelauscht wird. &#039;&#039;0.0.0.0&#039;&#039; = alle IPv4-Adressen; &#039;&#039;127.0.0.1&#039;&#039; = nur lokal; explizite IPs binden gezielt auf eine Netzwerkkarte&lt;br /&gt;
|-&lt;br /&gt;
| Port    || TCP-Port (1-65535). 443/8443 für HTTPS, frei wählbar für Debug-Profile&lt;br /&gt;
|-&lt;br /&gt;
| Aktiv   || Bindung wird beim Server-Start geladen&lt;br /&gt;
|-&lt;br /&gt;
| Standard|| Markiert die Standard-Bindung des Servers. Pro Server-Profil ist nur eine Standard-Bindung erlaubt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Wirksamkeit===&lt;br /&gt;
&lt;br /&gt;
Bindungen werden ausschliesslich beim Start des REST-Dienstes geladen. Änderungen werden daher erst nach einem Neustart aktiv. Bis dahin laufen die alten Bindungen weiter.&lt;br /&gt;
&lt;br /&gt;
==Listen-Funktionen==&lt;br /&gt;
&lt;br /&gt;
In der Server-Liste:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfg&#039;&#039;&#039; - neues Server-Profil&lt;br /&gt;
* &#039;&#039;&#039;Return&#039;&#039;&#039; - vorhandenes Profil bearbeiten&lt;br /&gt;
* &#039;&#039;&#039;F4&#039;&#039;&#039; - Sortierung wählen&lt;br /&gt;
* &#039;&#039;&#039;F6&#039;&#039;&#039; - Bindungs-Liste des markierten Servers&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=65544</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Einrichtung</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Einrichtung&amp;diff=65544"/>
		<updated>2026-09-25T09:28:33Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Wartung der Idempotenz-Tabelle berichtigt, Stolperstein fehlender Idempotency-Key ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Einrichtung eines REST-Endpunktes=&lt;br /&gt;
&lt;br /&gt;
Die folgende Reihenfolge stellt sicher, dass ein lauffähiger Endpunkt mit möglichst wenigen Korrektur-Schleifen entsteht.&lt;br /&gt;
&lt;br /&gt;
==Vorbereitung==&lt;br /&gt;
&lt;br /&gt;
# OBS-Support kontaktieren und das REST-Server-Modul aktivieren lassen.&lt;br /&gt;
# Prüfen, ob auf dem Zielrechner (OBS-Server oder dedizierter REST-Server) Port und IP-Adresse frei sind.&lt;br /&gt;
# Bei TLS-Profilen: TLS-Zertifikat (PEM, ggf. mit Chain), privaten Schlüssel (PEM) und - falls mTLS - das CA-Root-Zertifikat bereithalten.&lt;br /&gt;
&lt;br /&gt;
==Schritt 1: Server-Profil anlegen==&lt;br /&gt;
&lt;br /&gt;
In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server&#039;&#039;&#039; den Punkt &#039;&#039;&#039;Server&#039;&#039;&#039; öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; ein neues Profil anlegen. Pflichtfelder:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Nr&#039;&#039;&#039; - eindeutige Nummer (1-99), wird beim Anlegen automatisch vorgeschlagen.&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - sprechender Name (z.B. &#039;&#039;Public-API&#039;&#039;, &#039;&#039;Internal-mTLS&#039;&#039;, &#039;&#039;Debug-Local&#039;&#039;).&lt;br /&gt;
* Auswahl des Modus:&lt;br /&gt;
** &#039;&#039;&#039;Standard-TLS&#039;&#039;&#039; (Default): Zertifikat + Key + Root-Zertifikat hinterlegen.&lt;br /&gt;
** &#039;&#039;&#039;mTLS&#039;&#039;&#039; aktivieren: zusätzlich CA-Root-Zertifikat hinterlegen, jeder Client muss ein gültiges Cert vorlegen.&lt;br /&gt;
** &#039;&#039;&#039;Kein SSL&#039;&#039;&#039; aktivieren: ausschliesslich für lokale Tests, der Server akzeptiert dann nur Plain-HTTP.&lt;br /&gt;
&lt;br /&gt;
Optional lassen sich am Profil eigene &#039;&#039;&#039;Rate-Limit-Schwellen&#039;&#039;&#039; hinterlegen. Ohne&lt;br /&gt;
Angabe gelten die Standardwerte (60-Sekunden-Fenster, 120 erfolgreiche und 10&lt;br /&gt;
fehlgeschlagene Anfragen je Schlüssel, 600 Anfragen je Profil). Für Anwendungen&lt;br /&gt;
ohne eigenen API-Key - typisch mobile Apps - sollten die Werte bewusst gesetzt&lt;br /&gt;
werden, weil dann alle Benutzer hinter einer IP auf denselben Zähler laufen.&lt;br /&gt;
&lt;br /&gt;
Details siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 2: Bindung anlegen==&lt;br /&gt;
&lt;br /&gt;
Im Server-Profil mit &#039;&#039;&#039;F6&#039;&#039;&#039; die Bindungs-Liste öffnen und mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; eine neue Bindung anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - IP-Adresse, auf der gelauscht werden soll (z.B. &#039;&#039;0.0.0.0&#039;&#039; für alle Adressen, &#039;&#039;127.0.0.1&#039;&#039; für lokal).&lt;br /&gt;
* &#039;&#039;&#039;Port&#039;&#039;&#039; - Port-Nummer (typisch 443 für HTTPS, 8099 für Debug, ggf. abweichend). {{Achtung|Port 8080 wird für RustDesk verwendet und darf daher nicht verwendet werden}}&lt;br /&gt;
* &#039;&#039;&#039;Standard&#039;&#039;&#039; setzen, wenn diese Bindung die Standard-Bindung des Servers ist (pro Server-Profil nur eine).&lt;br /&gt;
&lt;br /&gt;
==Schritt 3: REST-Dienst starten / neustarten==&lt;br /&gt;
&lt;br /&gt;
Damit Änderungen an Server-Profilen oder Bindungen wirksam werden, muss der OBS REST-Server-Dienst (oder die Konsole) gestartet bzw. neugestartet werden. Das Profil wird beim Start aus der Datenbank gelesen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 4: Zugang anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Zugänge&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Zugang anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; - erscheint im Protokoll und in der Statistik.&lt;br /&gt;
* &#039;&#039;&#039;API-Key&#039;&#039;&#039; - eindeutig, mit dem Schlüsselsymbol kann ein zufälliger Key generiert werden.&lt;br /&gt;
* &#039;&#039;&#039;Host&#039;&#039;&#039; - optional die IP oder der Hostname des Konsumenten (sollte, wenn möglich, immer gesetzt werden).&lt;br /&gt;
* Optional: &#039;&#039;&#039;CORS-Origins&#039;&#039;&#039; und &#039;&#039;&#039;JWT-Konfiguration&#039;&#039;&#039;, siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Schritt 5: Endpunkt anlegen==&lt;br /&gt;
&lt;br /&gt;
Unter &#039;&#039;&#039;Endpunkte&#039;&#039;&#039; mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; einen neuen Endpunkt anlegen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Endpunkt&#039;&#039;&#039; - Ressourcen-/Anzeigename (z.B. &#039;&#039;orders&#039;&#039;, &#039;&#039;kalender&#039;&#039;); dient nur der Übersicht, nicht dem Routing.&lt;br /&gt;
* &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; - der maßgebliche Routing-Pfad, ggf. mit Platzhaltern (z.B. &#039;&#039;/orders&#039;&#039;, &#039;&#039;/orders/{uid}&#039;&#039;, &#039;&#039;/orders/{uid}/modules/{code}&#039;&#039;).&lt;br /&gt;
* &#039;&#039;&#039;Server&#039;&#039;&#039; - die Server-Auswahl bestimmt, über welches Server-Profil der Endpunkt erreichbar ist.&lt;br /&gt;
* &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; - nur setzen, wenn der Endpunkt Dateien entgegennehmen soll; dann auch die &#039;&#039;&#039;Max. Upload-Grösse&#039;&#039;&#039; in MB festlegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6]]).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Eine Versionskennung wird als statisches Segment ins Pfad-Template aufgenommen (z.B. &#039;&#039;/orders/v1&#039;&#039;); ein eigenes Versionsfeld gibt es nicht. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].}}&lt;br /&gt;
&lt;br /&gt;
Anschliessend mit &#039;&#039;&#039;F7&#039;&#039;&#039; das Endpunkt-Skript pflegen (siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]]).&lt;br /&gt;
&lt;br /&gt;
==Schritt 6: Zugang für den Endpunkt freischalten==&lt;br /&gt;
&lt;br /&gt;
Aus der Endpunkt-Liste den neuen Endpunkt markieren und &#039;&#039;&#039;F6&#039;&#039;&#039; drücken. Es öffnet sich die Berechtigungs-Liste. Mit &#039;&#039;&#039;Einfg&#039;&#039;&#039; kann ein oder mehrere Zugänge ausgewählt und mit &#039;&#039;&#039;F2&#039;&#039;&#039; übernommen werden. Erst nach dieser Freischaltung darf ein Zugang den Endpunkt aufrufen.&lt;br /&gt;
&lt;br /&gt;
==Schritt 7: Funktionsprüfung==&lt;br /&gt;
&lt;br /&gt;
Aufruf mit einem HTTP-Client (curl, Postman, Browser-Plug-in):&lt;br /&gt;
&lt;br /&gt;
 curl -H &amp;quot;apikey: [API-KEY]&amp;quot; https://api.meinserver.de/[Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Bei korrektem Setup erscheint die JSON-Antwort des Skripts. Fehlerfälle werden in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; protokolliert.&lt;br /&gt;
&lt;br /&gt;
==Schritt 8: Statistik und Protokoll prüfen==&lt;br /&gt;
&lt;br /&gt;
* In der Endpunkt-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Endpunkts.&lt;br /&gt;
* In der Zugänge-Liste &#039;&#039;&#039;F8&#039;&#039;&#039; für die Statistik des Zugangs.&lt;br /&gt;
* In &#039;&#039;&#039;Stammdaten -&amp;gt; Z Weitere Stammdaten -&amp;gt; REST-Server -&amp;gt; Protokoll&#039;&#039;&#039; die Ereignisse prüfen.&lt;br /&gt;
&lt;br /&gt;
==Wartung der Server-Tabellen==&lt;br /&gt;
&lt;br /&gt;
Die Tabellen des REST-Servers pflegen sich selbst; ein regelmässiger Eingriff ist&lt;br /&gt;
nicht vorgesehen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Tabelle !! Aufräumen&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_IDEMPOTENCY || Abgeschlossene Einträge werden nach 30 Tagen gelöscht. Offene Reservierungen werden ab 24 Stunden im Protokoll gemeldet und &#039;&#039;&#039;anschliessend ebenfalls gelöscht&#039;&#039;&#039; - sie sind trotzdem ein Support-Fall, weil die Fachwirkung unbekannt ist, siehe Stolpersteine&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_TOKEN || Zeilen werden gelöscht, sobald Access- &#039;&#039;&#039;und&#039;&#039;&#039; Refresh-Laufzeit vorbei sind. Läuft beim Serverstart und danach höchstens stündlich&lt;br /&gt;
|-&lt;br /&gt;
| RESTSRV_PROTO / RESTSRV_STATS || Wachsen mit dem Betrieb. Bei hohem Aufkommen empfiehlt sich ein turnusmässiges Archivieren&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Häufige Stolpersteine==&lt;br /&gt;
&lt;br /&gt;
* Bindung angelegt, aber der Server wurde nicht neugestartet -&amp;gt; die Bindung ist noch nicht aktiv.&lt;br /&gt;
* Endpunkt einem falschen Server zugeordnet -&amp;gt; Aufruf liefert 404 &#039;&#039;Endpunkt nicht vorhanden oder inaktiv&#039;&#039;.&lt;br /&gt;
* Pfad-Template falsch geschrieben oder Segmentzahl passt nicht -&amp;gt; kein Template matcht, Aufruf liefert 404.&lt;br /&gt;
* Zugang für den Endpunkt nicht freigeschaltet -&amp;gt; Aufruf liefert 403 &#039;&#039;Keine Berechtigung für die Endpunkt-Nutzung&#039;&#039;.&lt;br /&gt;
* Falscher API-Key oder falscher Host beim Zugang -&amp;gt; Aufruf liefert 401 bzw. 403.&lt;br /&gt;
* mTLS aktiv, aber kein Client-Zertifikat installiert -&amp;gt; Verbindung wird während des TLS-Handshakes abgebrochen.&lt;br /&gt;
* Datei-Upload ohne gesetzten Haken &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; am Endpunkt -&amp;gt; Aufruf liefert 415, das Skript läuft nicht an.&lt;br /&gt;
* Skript geändert, aber der Aufruf verhält sich noch wie vorher -&amp;gt; Endpunkt-Cache abwarten (bis zu 60 Sekunden).&lt;br /&gt;
* Schreibaufruf (POST/PUT/PATCH/DELETE) liefert 400 &#039;&#039;IDEMPOTENCY_KEY_MISSING&#039;&#039; -&amp;gt; der Client sendet keinen &#039;&#039;Idempotency-Key&#039;&#039;. Der Header ist seit v1.21 &#039;&#039;&#039;Pflicht&#039;&#039;&#039;, nicht optional; das Endpunkt-Skript läuft in diesem Fall gar nicht an. Betrifft typischerweise Konsumenten, die vor dem Update gebaut wurden.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 422 &#039;&#039;IDEMPOTENCY_KEY_REUSED&#039;&#039; -&amp;gt; der Client sendet denselben &#039;&#039;Idempotency-Key&#039;&#039; mit geändertem Inhalt. Pro Vorgang genau ein Schlüssel, über alle Wiederholversuche hinweg unverändert.&lt;br /&gt;
* Wiederholter Schreibaufruf liefert 409 &#039;&#039;IDEMPOTENCY_UNRESOLVED&#039;&#039; -&amp;gt; ein früherer Versuch wurde hart abgebrochen (Dienst beendet). Ob gebucht wurde, ist offen: im Protokoll und in den Fachdaten prüfen. Der Aufräumlauf meldet den Eintrag nach 24 Stunden im Protokoll und löscht ihn dann; wer schneller weitermachen will, entfernt ihn in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;.&lt;br /&gt;
* Nach einem Update antworten &#039;&#039;&#039;alle&#039;&#039;&#039; bestehenden Token mit 401 &#039;&#039;AUTH_EXPIRED&#039;&#039; -&amp;gt; erwartetes Verhalten. Token, die vor der Einführung der Sitzungsprüfung ausgestellt wurden, gehören zu keiner Sitzung; die Konsumenten müssen sich einmalig neu anmelden.&lt;br /&gt;
* Anmeldung liefert 503 statt eines Tokens -&amp;gt; die Sitzungszeile konnte nicht geschrieben werden. Ursache im Protokoll unter &#039;&#039;TokenStore: ...&#039;&#039; nachsehen; typisch ist eine fehlende Tabelle &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; nach einem Update ohne Datenbank-Abgleich.&lt;br /&gt;
* Abmelden liefert 503 -&amp;gt; die Sitzung liess sich nicht sperren. Der Client ist &#039;&#039;&#039;nicht&#039;&#039;&#039; abgemeldet und muss den Aufruf wiederholen; Details im Protokoll.&lt;br /&gt;
* Techniker wird beim Arbeiten unerwartet abgemeldet -&amp;gt; im Protokoll nach &#039;&#039;WIEDERVORLAGE&#039;&#039; suchen. Ein doppelt verwendetes Refresh-Token sperrt die Sitzung. Legt die App Token mehrfach parallel ab, gehört das clientseitig behoben.&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Scripting&amp;diff=65543</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Scripting</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Scripting&amp;diff=65543"/>
		<updated>2026-09-25T09:28:33Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Idempotenz bei 5xx berichtigt (Schluessel wird freigegeben), Pflichtheader und Dateiantwort ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
=Anleitung für Endpunkt-Skripte=&lt;br /&gt;
&lt;br /&gt;
Jede Anfrage an einen Endpunkt wird nach erfolgreicher Authentifizierung und Autorisierung an das hinterlegte Skript weitergegeben. Das Skript ist in Object Pascal geschrieben und greift auf die OBS-Bibliothek (DB-Zugriff, Hilfsfunktionen) zu.&lt;br /&gt;
&lt;br /&gt;
==Sicherheitshinweise==&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Endpunkt-Skripte verarbeiten Daten aus dem Internet. Übergabeparameter dürfen niemals ungeprüft in SQL-Anweisungen eingebaut werden. Werte immer über &#039;&#039;DB_SQLVal&#039;&#039; bzw. Parameter-Bindings absichern, Eingabewerte gegen Whitelists prüfen.}}&lt;br /&gt;
&lt;br /&gt;
* Eingaben gegen erwartete Werte prüfen (z.B. Pflichtfelder, erlaubte Typen, Wertebereiche).&lt;br /&gt;
* Nur das zurückgeben, was der Konsument wirklich braucht - keine internen IDs, keine Sys-Felder, keine Passwörter.&lt;br /&gt;
* Bei Fehlern keine internen Details an den Konsumenten zurückgeben; stattdessen schreibt der Server ohnehin Detail-Einträge in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==Methoden-Signatur==&lt;br /&gt;
&lt;br /&gt;
Pro HTTP-Methode wird im Skript eine gleichnamige &#039;&#039;&#039;Prozedur&#039;&#039;&#039; implementiert. Der Server ruft genau die Prozedur auf, die zur Methode des eingehenden Requests passt.&lt;br /&gt;
&lt;br /&gt;
 procedure Get   (oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
 procedure Post  (oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
 procedure Put   (oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
 procedure Patch (oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
&lt;br /&gt;
Zwei Handles, kein Rückgabewert: &#039;&#039;&#039;oReader&#039;&#039;&#039; ist die Anfrage, &#039;&#039;&#039;oWriter&#039;&#039;&#039; die&lt;br /&gt;
Antwort. Beide erzeugt der Server, beide gehören ihm - im Skript wird nichts&lt;br /&gt;
angelegt und nichts freigegeben.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;DELETE hat keine Skript-Methode.&#039;&#039;&#039; &amp;lt;code&amp;gt;Delete&amp;lt;/code&amp;gt; ist in der&lt;br /&gt;
Skriptsprache eine Standardprozedur (Löschen aus einer Zeichenkette); eine eigene&lt;br /&gt;
Prozedur dieses Namens lässt sich nicht übersetzen, und der Fehler nennt nur die&lt;br /&gt;
Zeile der Deklaration. Wer einen DELETE-Endpunkt braucht, legt die Methode unter&lt;br /&gt;
einem anderen Verb ab oder trennt sie in ein eigenes Skript.}}&lt;br /&gt;
&lt;br /&gt;
Fehlt die zur Methode passende Funktion, antwortet der Server mit &#039;&#039;&#039;405 Method&lt;br /&gt;
Not Allowed&#039;&#039;&#039; (&amp;lt;code&amp;gt;METHOD_NOT_ALLOWED&amp;lt;/code&amp;gt;) und nennt im Header&lt;br /&gt;
&amp;lt;code&amp;gt;Allow&amp;lt;/code&amp;gt; die Verben, die dieses Skript tatsächlich anbietet - die Liste&lt;br /&gt;
stammt aus dem kompilierten Skript und kann deshalb nicht veralten:&lt;br /&gt;
&lt;br /&gt;
 HTTP/1.1 405 Method Not Allowed&lt;br /&gt;
 Allow: GET, PUT&lt;br /&gt;
&lt;br /&gt;
Ein im Vertrag zugesagtes Verb braucht also seine Prozedur. Fehlt sie, ist das an&lt;br /&gt;
der Antwort sofort zu erkennen und &#039;&#039;&#039;kein&#039;&#039;&#039; Serverfehler.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Die Anfrage lesen: oReader==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;oReader&#039;&#039; ist der einzige Zugang zur Anfrage - Query, Header, Formularfelder,&lt;br /&gt;
Pfadwerte, Token-Claims und Körper. Alles davon sind &#039;&#039;&#039;Zeichenketten&#039;&#039;&#039; oder&lt;br /&gt;
werden über einen typisierten Leser geholt.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Was er liefert&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;kundennr&#039;)&amp;lt;/code&amp;gt; || Query-Parameter, POST-Parameter (form-urlencoded) oder HTTP-Header. Getrimmt, leer wenn nicht gesendet&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Path(&#039;uid&#039;)&amp;lt;/code&amp;gt; || Wert eines Platzhalters aus dem Pfad-Template&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Form(&#039;bemerkung&#039;)&amp;lt;/code&amp;gt; || Formularfeld einer &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt;-Übertragung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Claim(&#039;tenant&#039;)&amp;lt;/code&amp;gt; || Custom-Claim des vorgelegten Tokens&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Subject()&amp;lt;/code&amp;gt; || Subject des Tokens (&#039;&#039;sub&#039;&#039;-Claim)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.TraceId()&amp;lt;/code&amp;gt; || Korrelations-ID des Requests&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Offset()&amp;lt;/code&amp;gt; || UTC-Offset des Servers, z.B. &#039;&#039;+02:00&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.HasBody()&amp;lt;/code&amp;gt; || Wurde überhaupt ein JSON-Körper gesendet?&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Filterung durch den Server:&lt;br /&gt;
&lt;br /&gt;
* Geblockte Header werden nicht durchgereicht: &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, &#039;&#039;proxy-authorization&#039;&#039;, &#039;&#039;x-forwarded-for&#039;&#039;, &#039;&#039;x-real-ip&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;api_key&#039;&#039;.&lt;br /&gt;
* Werte, die von aussen kommen, können die reservierten Namen nicht besetzen.&lt;br /&gt;
* Werte werden auf max. 1024 Zeichen begrenzt.&lt;br /&gt;
* Null-Bytes und Steuerzeichen (ausser Tab, CR, LF) werden entfernt.&lt;br /&gt;
&lt;br /&gt;
Zugriff im Skript:&lt;br /&gt;
&lt;br /&gt;
 cKundenNr := oReader.Param(&#039;kundennr&#039;);&lt;br /&gt;
 if (not Empty(cKundenNr)) then begin&lt;br /&gt;
     // ...&lt;br /&gt;
 end;&lt;br /&gt;
&lt;br /&gt;
===Der Körper: Zugriff über Pfade===&lt;br /&gt;
&lt;br /&gt;
Den JSON-Körper liest &#039;&#039;oReader&#039;&#039; über &#039;&#039;&#039;Pfade&#039;&#039;&#039;. Es gibt keine Unterobjekte im&lt;br /&gt;
Skript, keinen Cast, kein &#039;&#039;.Items[i]&#039;&#039; - und damit auch keine Lebensdauer, um die&lt;br /&gt;
sich jemand kümmern müsste.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Str(&#039;betreff&#039;, cWert)&amp;lt;/code&amp;gt; || Liest den Wert. &#039;&#039;&#039;Rückgabe Boolean&#039;&#039;&#039;: war das Feld da? Fehlt es, bleibt die Variable unverändert&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Int(&#039;menge&#039;, nWert)&amp;lt;/code&amp;gt; || dito, Ganzzahl&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Num(&#039;preis&#039;, nWert)&amp;lt;/code&amp;gt; || dito, Kommazahl. &#039;&#039;&#039;Der Dezimalpunkt aus JSON wird richtig gelesen&#039;&#039;&#039; - kein &#039;&#039;StrTran&#039;&#039; mehr&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Bool(&#039;aktiv&#039;, lWert)&amp;lt;/code&amp;gt; || dito, Boolean&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.NumDef(&#039;rabatt&#039;, nWert, 0)&amp;lt;/code&amp;gt; || wie &#039;&#039;Num&#039;&#039;, mit Vorgabewert&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Has(&#039;feld&#039;)&amp;lt;/code&amp;gt; || Ist das Feld vorhanden? (auch bei &#039;&#039;null&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Count(&#039;zeiten&#039;)&amp;lt;/code&amp;gt; || Anzahl Einträge einer Liste, 0 wenn keine Liste&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.IsArr(&#039;zeiten&#039;)&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;IsObj(&#039;zeiten[0]&#039;)&amp;lt;/code&amp;gt; || Gestalt prüfen&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Keys()&amp;lt;/code&amp;gt; || Alle Feldnamen der obersten Ebene, in Kommas eingefasst&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Verschachtelung steht im Pfad&#039;&#039;&#039; - Punkt für Felder, eckige Klammern für Listen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Put(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cVon : string;&lt;br /&gt;
    nAnz : Integer;&lt;br /&gt;
    nI   : Integer;&lt;br /&gt;
begin&lt;br /&gt;
    nAnz := oReader.Count(&#039;zeiten&#039;);&lt;br /&gt;
    for nI := 0 to nAnz - 1 do begin&lt;br /&gt;
        if (oReader.Str(&#039;zeiten[&#039; + IntToStr(nI) + &#039;].von&#039;, cVon)) then begin&lt;br /&gt;
            // ... cVon verarbeiten ...&lt;br /&gt;
        end;&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Der Rückgabewert unterscheidet „nicht gesendet&amp;quot; von „leer&lt;br /&gt;
gesendet&amp;quot;.&#039;&#039;&#039; &amp;lt;code&amp;gt;oReader.Str(&#039;feld&#039;, cWert)&amp;lt;/code&amp;gt; liefert bei&lt;br /&gt;
&amp;lt;code&amp;gt;&amp;quot;feld&amp;quot;: null&amp;lt;/code&amp;gt; &#039;&#039;true&#039;&#039; mit leerem Wert und bei fehlendem Feld&lt;br /&gt;
&#039;&#039;false&#039;&#039;. Genau diese Unterscheidung braucht ein PATCH-artiger Aufruf: „Feld&lt;br /&gt;
löschen&amp;quot; und „Feld nicht anfassen&amp;quot; sehen sonst gleich aus.}}&lt;br /&gt;
&lt;br /&gt;
Ist gar kein Körper gesendet worden, liefert jeder Leser &#039;&#039;false&#039;&#039; und&lt;br /&gt;
&#039;&#039;HasBody()&#039;&#039; ist &#039;&#039;false&#039;&#039;. Maximale Body-Grösse: 10 MB - bei einem gepackt gesendeten Körper zusätzlich das 400fache der gesendeten Grösse, siehe [[OBS/Kostenpflichtige Module/RESTServer|REST-Server]], Abschnitt Kompression. Beide Überschreitungen beantwortet der Server mit &#039;&#039;&#039;413&#039;&#039;&#039;, das Skript läuft gar nicht erst an.&lt;br /&gt;
&lt;br /&gt;
===Werte des Servers===&lt;br /&gt;
&lt;br /&gt;
Bei aktiver JWT-Authentifizierung stehen die Token-Claims über den Reader zur&lt;br /&gt;
Verfügung. Sie tragen intern weiterhin die Namen mit Präfix &#039;&#039;&#039;_OBS_&#039;&#039;&#039; - im&lt;br /&gt;
Skript werden sie aber über die Zugriffe der Tabelle oben gelesen und nicht mehr&lt;br /&gt;
über den Namen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Parameter            !! Inhalt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;_OBS_JWT_ID&#039;)&amp;lt;/code&amp;gt; || JWT-Id (&#039;&#039;jti&#039;&#039;-Claim) - typisch die User-Id. &#039;&#039;&#039;Muss nicht eindeutig sein&#039;&#039;&#039;: die Sitzung führt der Server über eigene Kennungen (siehe unten)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Subject()&amp;lt;/code&amp;gt; || Subject (&#039;&#039;sub&#039;&#039;-Claim) - typisch Benutzername&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;_OBS_JWT_AUDIENCE&#039;)&amp;lt;/code&amp;gt; || Audience (&#039;&#039;aud&#039;&#039;-Claim) - typisch Mandant / Rolle&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Claim(&#039;tenant&#039;)&amp;lt;/code&amp;gt; || Beliebiger Custom-Claim des Tokens, hier &#039;&#039;tenant&#039;&#039;; im Folge-Skript lesbar&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.TraceId()&amp;lt;/code&amp;gt; || Korrelations-ID des Requests (auch als Response-Header X-Trace-Id)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Unabhängig von JWT stehen ausserdem immer zur Verfügung:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Parameter            !! Inhalt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;_OBS_SERVER_TIME&#039;)&amp;lt;/code&amp;gt; || Aktuelle Serverzeit als ISO-8601 &#039;&#039;&#039;mit UTC-Offset&#039;&#039;&#039; (z.B. &#039;&#039;2026-08-17T09:12:33+02:00&#039;&#039;). Nützlich, um Datumswerte in derselben Zeitzone auszuliefern, in der der Server arbeitet, und damit ein Client seinen Uhren-Versatz bestimmen kann&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Offset()&amp;lt;/code&amp;gt; || Nur der Offset daraus, z.B. &#039;&#039;+02:00&#039;&#039;. Er geht in die Zeitfunktionen (siehe [[#Zeitangaben auf der Leitung|Zeitangaben]])&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Param(&#039;_OBS_JWT_EXP_SEC&#039;)&amp;lt;/code&amp;gt; || Laufzeit des Access-Tokens in Sekunden (&#039;&#039;JWT-Exp&#039;&#039; × 60). Damit kann ein Konfigurations-Endpunkt denselben Wert ausliefern, den der Token wirklich hat&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Diese Werte werden vom Server gesetzt und können vom Skript für Berechtigungs- und Mandantenprüfungen verwendet werden.&lt;br /&gt;
&lt;br /&gt;
Zusätzlich trägt jedes Token zwei &#039;&#039;&#039;Sitzungskennungen des Servers&#039;&#039;&#039;, lesbar als&lt;br /&gt;
&#039;&#039;oReader.Claim(&#039;sid&#039;)&#039;&#039; und &#039;&#039;oReader.Claim(&#039;rid&#039;)&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Claim !! Inhalt&lt;br /&gt;
|-&lt;br /&gt;
| sid || Sitzung. Bleibt über alle Erneuerungen (Refresh) hinweg gleich und ist der Schlüssel für das Abmelden&lt;br /&gt;
|-&lt;br /&gt;
| rid || Zeilenkennung des einzelnen Token-Paares in &#039;&#039;RESTSRV_TOKEN&#039;&#039;. Bei jeder Erneuerung neu&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beide sind OBS-UIDs (10 Zeichen) und werden vom Server &#039;&#039;&#039;nach&#039;&#039;&#039; den Claims des&lt;br /&gt;
Skripts gesetzt - ein Skript kann sie also auch mit einem eigenen Claim &#039;&#039;sid&#039;&#039;&lt;br /&gt;
nicht überschreiben. Für Fachlogik sind sie nicht gedacht; sie sind nützlich, um eine&lt;br /&gt;
Sitzung im Protokoll wiederzufinden.&lt;br /&gt;
&lt;br /&gt;
===Pfad-Parameter===&lt;br /&gt;
&lt;br /&gt;
Stammt der Endpunkt aus einem Pfad-Template mit Platzhaltern (siehe [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]), stehen die aus den Platzhaltern erfassten Werte über &#039;&#039;oReader.Path&#039;&#039; bereit:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Template !! Zugriff im Skript&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;oReader.Path(&#039;uid&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders/{uid}/modules/{code}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;oReader.Path(&#039;uid&#039;)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;oReader.Path(&#039;code&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Diese Werte kommen aus dem Routing und sind durch den Client nicht fälschbar. Ein vollständiges Beispiel zeigt [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4 - Pfad-Parameter]].&lt;br /&gt;
&lt;br /&gt;
===Datei-Uploads===&lt;br /&gt;
&lt;br /&gt;
Ist der Endpunkt für Uploads freigeschaltet (&amp;lt;code&amp;gt;re_upload = 1&amp;lt;/code&amp;gt;, siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]), nimmt der Server&lt;br /&gt;
hochgeladene Dateien entgegen, legt sie in einem temporären Verzeichnis ab und&lt;br /&gt;
übergibt dem Skript Pfad, Originalname und Content-Type über den Reader. Das&lt;br /&gt;
Skript entscheidet selbst über die weitere Verarbeitung (z.B. DMS-Ablage). Der&lt;br /&gt;
Body wird in diesem Fall &#039;&#039;&#039;nicht&#039;&#039;&#039; als JSON geparst - &#039;&#039;oReader.HasBody()&#039;&#039; ist&lt;br /&gt;
&#039;&#039;false&#039;&#039;. Es gilt nicht das JSON-Body-Limit (10&amp;amp;nbsp;MB), sondern die pro&lt;br /&gt;
Endpunkt konfigurierte Grösse (&#039;&#039;re_upload_size&#039;&#039;, Standard 25&amp;amp;nbsp;MB;&lt;br /&gt;
Überschreitung -&amp;gt; 413).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Wie die Datei übertragen wurde, spielt für das Skript keine Rolle.&#039;&#039;&#039; Beide&lt;br /&gt;
Wege - &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; und der resumable&lt;br /&gt;
&amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;-Upload - liefern dieselben vier Werte. Das&lt;br /&gt;
Übertragungsprotokoll samt Header, Statuscodes und Resume-Verhalten steht in&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]]:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Inhalt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadPath()&amp;lt;/code&amp;gt; || Vollständiger Pfad zur temporären Datei auf dem Server&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadName()&amp;lt;/code&amp;gt; || Originaldateiname&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadType()&amp;lt;/code&amp;gt; || Content-Type der Datei (Default &amp;lt;code&amp;gt;application/octet-stream&amp;lt;/code&amp;gt;, wenn der Client keinen angibt), kleingeschrieben&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.UploadSha()&amp;lt;/code&amp;gt; || SHA-256 der &#039;&#039;&#039;gespeicherten&#039;&#039;&#039; Datei, hex in Kleinbuchstaben. Damit lässt sich eine vom Client mitgesendete Prüfsumme vergleichen - erst dieser Vergleich macht aus ihr eine Zusicherung&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Der &#039;&#039;&#039;Dateiname ist Pflicht&#039;&#039;&#039;. Fehlt er, lehnt der Server den Upload mit&lt;br /&gt;
&#039;&#039;&#039;400 Bad Request&#039;&#039;&#039; ab und das Skript wird nicht ausgeführt - das Skript kann&lt;br /&gt;
sich also darauf verlassen, dass &#039;&#039;UploadPath()&#039;&#039; und &#039;&#039;UploadName()&#039;&#039; gefüllt&lt;br /&gt;
sind, sobald es läuft.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Die temporäre Datei wird nicht automatisch verschoben. Das Skript muss sie an ihren Zielort (DMS, Verzeichnis, ...) übernehmen.}}&lt;br /&gt;
&lt;br /&gt;
Zusatzangaben, die im selben Aufruf mitkommen, liest das Skript mit&lt;br /&gt;
&amp;lt;code&amp;gt;oReader.Form(&#039;&amp;amp;lt;name&amp;amp;gt;&#039;)&amp;lt;/code&amp;gt; - bei &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; sind&lt;br /&gt;
das die Partien ohne &amp;lt;code&amp;gt;filename=&amp;lt;/code&amp;gt;. Ein vollständiges Beispiel zeigt&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6]].&lt;br /&gt;
&lt;br /&gt;
==Die Antwort schreiben: oWriter==&lt;br /&gt;
&lt;br /&gt;
Der Writer schreibt &#039;&#039;&#039;sequentiell&#039;&#039;&#039;: jeder Aufruf hängt ein Feld an, an der&lt;br /&gt;
Stelle, an der er steht. Die äussere Klammer der Antwort setzt der Server. Wird&lt;br /&gt;
nichts geschrieben, antwortet er mit &#039;&#039;{}&#039;&#039;. Content-Type ist&lt;br /&gt;
&#039;&#039;application/json; charset=utf-8&#039;&#039;, der Status 200, sofern das Skript nichts&lt;br /&gt;
anderes setzt.&lt;br /&gt;
&lt;br /&gt;
Einfaches Beispiel:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    oWriter.Str(&#039;wert_string&#039;, &#039;123&#039;);&lt;br /&gt;
    oWriter.Int(&#039;wert_int&#039;   , 456);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Ergebnis&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Str(&#039;name&#039;, cWert)&amp;lt;/code&amp;gt; || Zeichenkette, immer maskiert&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.StrN(&#039;name&#039;, cWert)&amp;lt;/code&amp;gt; || wie &#039;&#039;Str&#039;&#039;, aber &#039;&#039;&#039;null&#039;&#039;&#039; statt einer leeren Zeichenkette&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Int(&#039;anz&#039;, nWert)&amp;lt;/code&amp;gt; || Ganzzahl&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Num(&#039;preis&#039;, nWert, 2)&amp;lt;/code&amp;gt; || Kommazahl mit fester Stellenzahl. &#039;&#039;&#039;Immer mit Dezimalpunkt&#039;&#039;&#039;, unabhängig von der Locale des Servers&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Bool(&#039;aktiv&#039;, lWert)&amp;lt;/code&amp;gt; || &#039;&#039;true&#039;&#039; / &#039;&#039;false&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Null(&#039;lat&#039;)&amp;lt;/code&amp;gt; || ausdrücklich &#039;&#039;null&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.ObjBegin(&#039;kunde&#039;)&amp;lt;/code&amp;gt; … &amp;lt;code&amp;gt;oWriter.ObjEnd&amp;lt;/code&amp;gt; || Unterobjekt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.ArrBegin(&#039;dateien&#039;)&amp;lt;/code&amp;gt; … &amp;lt;code&amp;gt;oWriter.ArrEnd&amp;lt;/code&amp;gt; || Liste&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.RootArr()&amp;lt;/code&amp;gt; || Die &#039;&#039;&#039;Wurzel&#039;&#039;&#039; ist eine Liste statt eines Objekts. Muss vor dem ersten Feld stehen; ein &#039;&#039;ArrEnd&#039;&#039; gibt es dazu nicht&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Raw(&#039;meta&#039;, cJson&#039;)&amp;lt;/code&amp;gt; || Fertiges JSON-Fragment einbetten. Siehe unten&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;In einer Liste hat ein Eintrag keinen Namen&#039;&#039;&#039; - der leere Feldname ist genau&lt;br /&gt;
das:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
oWriter.ArrBegin(&#039;dateien&#039;);&lt;br /&gt;
    while (not q.EoF) do begin&lt;br /&gt;
        oWriter.ObjBegin(&#039;&#039;);&lt;br /&gt;
            oWriter.Str (&#039;uid&#039;      , q.A2C(&#039;f_uid&#039;));&lt;br /&gt;
            oWriter.Int (&#039;bytes&#039;    , q.A2I(&#039;f_bytes&#039;));&lt;br /&gt;
            oWriter.StrN(&#039;kategorie&#039;, AllTrim(q.A2C(&#039;f_kat&#039;)));   // null wenn leer&lt;br /&gt;
        oWriter.ObjEnd;&lt;br /&gt;
        q.Next();&lt;br /&gt;
    end;&lt;br /&gt;
oWriter.ArrEnd;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ein Name in einer Liste und ein fehlender Name in einem Objekt sind beides&lt;br /&gt;
&#039;&#039;&#039;Strukturfehler&#039;&#039;&#039;. Der Server antwortet dann mit &#039;&#039;&#039;500&#039;&#039;&#039; und nennt die&lt;br /&gt;
Stelle im Protokoll - er liefert kein halbes JSON aus. Dasselbe gilt für einen&lt;br /&gt;
Stapel, der am Ende der Methode noch offen ist.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Ein Helfer bekommt den Writer als Parameter&#039;&#039;&#039; und schreibt an der&lt;br /&gt;
Stelle hinein, an der er aufgerufen wird - er gibt kein Fragment zurück. Damit&lt;br /&gt;
steht die Reihenfolge der Antwort im Code und nicht im Zusammenbau:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;procedure DateienSchreiben(oWriter: TxRestWriter; const cNr: string);&amp;lt;/code&amp;gt;}}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Raw ist der Notausgang und prüft.&#039;&#039;&#039; Er parst das Fragment und bettet den&lt;br /&gt;
&#039;&#039;&#039;Originaltext&#039;&#039;&#039; ein; ungültiges JSON gibt einen Serverfehler, der das Feld&lt;br /&gt;
nennt. Gebraucht wird er für ein gespeichertes, &#039;&#039;&#039;signiertes&#039;&#039;&#039; Dokument:&lt;br /&gt;
Parsen und Neuserialisieren würde die signierte Feldreihenfolge zerstören.&lt;br /&gt;
&lt;br /&gt;
===Zeitangaben auf der Leitung===&lt;br /&gt;
&lt;br /&gt;
Zeitstempel gehen als &#039;&#039;&#039;ISO-8601 mit Offset&#039;&#039;&#039; über die Leitung. Die Wandlung&lt;br /&gt;
gehört nicht ins Skript - dort saß der Fehler „zwei Stunden zu früh&amp;quot; an drei&lt;br /&gt;
Stellen gleichzeitig:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Zweck&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;RestIsoFromDate(dWert, cOffset)&amp;lt;/code&amp;gt; || OBS-Zeitpunkt -&amp;gt; &#039;&#039;2026-09-09T14:12:00+02:00&#039;&#039;. Leerer String bei leerem Datum&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;RestDateFromIso(cWert, cOffset)&amp;lt;/code&amp;gt; || ISO-Zeichenkette -&amp;gt; OBS-Zeitpunkt in &#039;&#039;&#039;Ortszeit des Servers&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oReader.Offset()&amp;lt;/code&amp;gt; || Der Offset, der in beide hineingeht&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;RestOffsetToMinutes(cOffset)&amp;lt;/code&amp;gt; || Offset in Minuten, für eigene Rechnungen&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Antwort steuern: Statuscode, Header, traceId==&lt;br /&gt;
&lt;br /&gt;
Statuscode, Header und das Sperren einer Sitzung laufen über eigene Aufrufe des&lt;br /&gt;
Writers, &#039;&#039;&#039;nicht&#039;&#039;&#039; über Felder im Körper.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Aufruf !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Status(201)&amp;lt;/code&amp;gt; || HTTP-Statuscode (z.B. 201, 204, 400, 409, 422). Ohne Angabe: 200&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Header(&#039;ETag&#039;, &#039;7&#039;)&amp;lt;/code&amp;gt; || Beliebiger Response-Header, z.B. ETag, Location, Retry-After. CR/LF in Namen und Werten werden entfernt (Schutz vor Header-Injection)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.NoBody()&amp;lt;/code&amp;gt; || Kein Körper - für &#039;&#039;&#039;204&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.JwtRevoke(&#039;session&#039;)&amp;lt;/code&amp;gt; || Sperrt die Sitzung des vorgelegten Tokens (&#039;&#039;session&#039;&#039;) oder alle Sitzungen des Subjects (&#039;&#039;all&#039;&#039;). Damit wird ein Endpunkt zum Logout, ohne eigene Verwaltung. Scheitert das Sperren, antwortet der Server mit &#039;&#039;&#039;503&#039;&#039;&#039; statt mit der Antwort des Skripts - siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]], Abschnitt Abmelden&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Feld „menge&amp;quot; fehlt&#039;)&amp;lt;/code&amp;gt; || Fertige Fehlerantwort in der Form des Servers, siehe [[#Fehlerbehandlung im Skript|Fehlerbehandlung]]&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.HasError()&amp;lt;/code&amp;gt; || Steht schon eine Fehlerantwort im Writer? Für einen Verteiler, der danach noch etwas anhängen will&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.SendFile(cPfad, cName, cTyp)&amp;lt;/code&amp;gt; || Statt eines JSON-Körpers eine &#039;&#039;&#039;Datei&#039;&#039;&#039; ausliefern. Die Datei bleibt liegen, der Download ist wiederaufsetzbar. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]]&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;oWriter.SendTempFile(cPfad, cName, cTyp)&amp;lt;/code&amp;gt; || Wie &amp;lt;code&amp;gt;SendFile&amp;lt;/code&amp;gt;, aber die Datei wird nach dem Senden &#039;&#039;&#039;gelöscht&#039;&#039;&#039; und der Download ist nicht wiederaufsetzbar - für Ergebnisse, die je Abruf entstehen (CSV-Export, Report)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Zusätzlich trägt jede Antwort den Header &#039;&#039;&#039;X-Trace-Id&#039;&#039;&#039; (Korrelations-ID). Dieselbe ID liegt dem Skript als &#039;&#039;oReader.TraceId()&#039;&#039; vor und erscheint in jeder Server-Fehler-Logzeile in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; - so lässt sich ein Fehler ohne Gerätezugriff im Log wiederfinden.&lt;br /&gt;
&lt;br /&gt;
Setzt ein Skript den Header &#039;&#039;&#039;Content-Encoding&#039;&#039;&#039; selbst - etwa weil es einen bereits gepackten Körper ausliefert -, hält sich die Engine heraus und packt die Antwort nicht noch einmal. Ohne diesen Header entscheidet der Server selbst, ob gzip zum Einsatz kommt.&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Anlegen mit 201, Location und ETag===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cNeueUid: string;&lt;br /&gt;
begin&lt;br /&gt;
    // ... Datensatz anlegen, neue UID + Version (ETag) ermitteln ...&lt;br /&gt;
&lt;br /&gt;
    oWriter.Str(&#039;uid&#039;, cNeueUid);&lt;br /&gt;
    oWriter.Status(201);&lt;br /&gt;
    oWriter.Header(&#039;Location&#039;, &#039;/v1/orders/&#039; + cNeueUid);&lt;br /&gt;
    oWriter.Header(&#039;ETag&#039;    , &#039;1&#039;);&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Strukturierter Fehler mit Statuscode und traceId===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var nMenge: Integer;&lt;br /&gt;
begin&lt;br /&gt;
    if (not oReader.Int(&#039;menge&#039;, nMenge)) then begin&lt;br /&gt;
        // Statuscode, code, message, uid und traceId in EINEM Aufruf - dieselbe&lt;br /&gt;
        // Huelle, die auch die Server-eigenen Fehler tragen.&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Feld &amp;quot;menge&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
    // ...&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Optimistic Concurrency (ETag / If-Match)===&lt;br /&gt;
&lt;br /&gt;
Mit Statuscode, Headern und dem lesbaren Header &#039;&#039;If-Match&#039;&#039; führt das Skript je Datensatz einen Versionszähler und lehnt veraltete Schreibzugriffe mit 409 ab:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Put(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var nAktuell: Integer;&lt;br /&gt;
    nIfMatch: Integer;&lt;br /&gt;
begin&lt;br /&gt;
    nAktuell := AuftragVersion(oReader.Path(&#039;uid&#039;));&lt;br /&gt;
    nIfMatch := iVal(oReader.Param(&#039;if-match&#039;));&lt;br /&gt;
&lt;br /&gt;
    if (nIfMatch &amp;lt;&amp;gt; nAktuell) then begin&lt;br /&gt;
        // Der ETag gehoert AUCH an die Ablehnung - sonst kennt der Client den&lt;br /&gt;
        // gueltigen Wert nicht und sein naechster Versuch scheitert wieder.&lt;br /&gt;
        oWriter.Header(&#039;ETag&#039;, xStr(nAktuell));&lt;br /&gt;
        oWriter.Error(409, &#039;VERSION_CONFLICT&#039;, &#039;Der Datensatz wurde zwischenzeitlich geaendert&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // ... speichern, Version hochzählen ...&lt;br /&gt;
    oWriter.Header(&#039;ETag&#039;, xStr(nAktuell + 1));&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Idempotenz - was das Skript beachten muss==&lt;br /&gt;
&lt;br /&gt;
Bei &#039;&#039;POST&#039;&#039;/&#039;&#039;PUT&#039;&#039;/&#039;&#039;PATCH&#039;&#039;/&#039;&#039;DELETE&#039;&#039; fängt der &#039;&#039;&#039;Server&#039;&#039;&#039; doppelte&lt;br /&gt;
Sendungen über den Header &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; ab. Das Skript braucht dafür&lt;br /&gt;
&#039;&#039;&#039;keine eigene Logik&#039;&#039;&#039; - keine Schlüssel-Tabelle, keine Prüfung am Anfang der&lt;br /&gt;
Methode.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Header ist bei diesen vier Verben &#039;&#039;&#039;Pflicht&#039;&#039;&#039;. Fehlt er, antwortet&lt;br /&gt;
der Server mit &#039;&#039;&#039;400&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt; - das Skript läuft&lt;br /&gt;
dann gar nicht erst an, es muss den Fall also auch nicht behandeln.}}&lt;br /&gt;
&lt;br /&gt;
Was das Skript wissen muss:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Antwort des Skripts !! Wirkung auf den Idempotenz-Speicher&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;2xx&#039;&#039;&#039; || Statuscode, Body und Header werden gespeichert. Eine Wiederholung mit demselben Schlüssel bekommt genau diese Antwort zurück, das Skript läuft nicht erneut&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;2xx&#039;&#039;&#039; mit &#039;&#039;SendFile&#039;&#039;/&#039;&#039;SendTempFile&#039;&#039; || &#039;&#039;&#039;Nichts&#039;&#039;&#039; wird gespeichert - eine Dateiantwort liesse sich nicht wiedergeben. Der Schlüssel wird freigegeben und ist danach wieder benutzbar&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;4xx&#039;&#039;&#039; || Der Schlüssel wird &#039;&#039;&#039;freigegeben&#039;&#039;&#039;. Der Konsument darf denselben Schlüssel nach Korrektur des Inhalts erneut verwenden&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;5xx&#039;&#039;&#039; oder Exception || Der Schlüssel wird ebenfalls &#039;&#039;&#039;freigegeben&#039;&#039;&#039; und der Fall protokolliert. Der Konsument darf denselben Schlüssel erneut senden&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Das 5xx-Verhalten wurde am 25.08.2026 gedreht.&#039;&#039;&#039; Vorher blieb der&lt;br /&gt;
Schlüssel nach einem Serverfehler belegt - begründet mit „niemals eine&lt;br /&gt;
Doppelbuchung&amp;quot;. Das war falsch gewichtet: der Vorgang wurde damit&lt;br /&gt;
&#039;&#039;&#039;unwiederholbar&#039;&#039;&#039;, der Client bekam dauerhaft 503 und hätte die erfasste Arbeit&lt;br /&gt;
verwerfen müssen. Garantierter Datenverlust wiegt schwerer als ein möglicher&lt;br /&gt;
Doppelsatz.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Folge für die Auslegung:&#039;&#039;&#039; Ein schreibender Endpunkt sollte selbst&lt;br /&gt;
wiederholbar sein, etwa über ein fachliches Merkmal des Clients. Wo reines&lt;br /&gt;
Anhängen stattfindet (Positionen, Dateien), bleibt sonst ein Restrisiko.}}&lt;br /&gt;
&lt;br /&gt;
Daraus folgen zwei Regeln für Endpunkt-Skripte:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Fachliche Ablehnungen als 4xx melden&#039;&#039;&#039; (422, 409, 404), nicht als 2xx mit Fehlertext im Body. Sonst wird die Ablehnung gespeichert und der Konsument bekommt sie bei jedem weiteren Versuch erneut - auch nachdem er den Fehler behoben hat.&lt;br /&gt;
* &#039;&#039;&#039;Die Antwort muss vollständig sein.&#039;&#039;&#039; Was zurückgegeben wird, wird eingefroren. Ein Feld, das erst der zweite Aufruf ergänzen würde, kommt beim Konsumenten nie an.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Der vollständige Ablauf und die Statuscodes stehen unter&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].&lt;br /&gt;
&lt;br /&gt;
==JWT-Authentifizierungs-Skript==&lt;br /&gt;
&lt;br /&gt;
Bei Zugängen mit aktiver JWT-Pflicht wird über den JWT-Endpunkt das im Zugang hinterlegte Authentifizierungs-Skript aufgerufen (F7 in der Zugänge-Liste). Das Skript muss eine Methode &#039;&#039;Authenticate&#039;&#039; bereitstellen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Authenticate(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUser: string;&lt;br /&gt;
    cPass: string;&lt;br /&gt;
begin&lt;br /&gt;
    // Eingangsdaten lesen&lt;br /&gt;
    cUser := &#039;&#039;;&lt;br /&gt;
    cPass := &#039;&#039;;&lt;br /&gt;
    oReader.Str(&#039;username&#039;, cUser);&lt;br /&gt;
    oReader.Str(&#039;password&#039;, cPass);&lt;br /&gt;
&lt;br /&gt;
    // Prüfung gegen eigene Tabelle, Hash-Verfahren, LDAP, ...&lt;br /&gt;
    if (PasswortPasst(cUser, cPass)) then begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;           , 1);&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_ID&#039;      , cUser);&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_SUBJECT&#039; , cUser);&lt;br /&gt;
        oWriter.Str(&#039;_OBS_JWT_AUDIENCE&#039;, &#039;mandant1&#039;);&lt;br /&gt;
    end else begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;, 9);&lt;br /&gt;
        oWriter.Str(&#039;error&#039; , &#039;Login fehlgeschlagen&#039;);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Im Anmeldeskript bleiben die _OBS_JWT_*-Felder Felder der&lt;br /&gt;
Antwort&#039;&#039;&#039; - anders als Status und Header, die zu Aufrufen des Writers geworden&lt;br /&gt;
sind. Der Grund: die Token-Ausstellung liest sie aus dem fertigen Körper, und&lt;br /&gt;
genau dort erwartet sie sie. Geschrieben werden sie deshalb wie jedes andere&lt;br /&gt;
Feld, nur eben mit dem Writer.}}&lt;br /&gt;
&lt;br /&gt;
Rückgabewerte:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! status !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| 1      || Erfolgreich, der Server erzeugt aus den &#039;&#039;_OBS_JWT_*&#039;&#039;-Werten einen Token (HS256)&lt;br /&gt;
|-&lt;br /&gt;
| 9      || Misserfolg, der Server antwortet mit &#039;&#039;&#039;401&#039;&#039;&#039; und reicht den Wert von &#039;&#039;error&#039;&#039; als &#039;&#039;error.message&#039;&#039; an den Client durch (zusätzlich protokolliert)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Text aus &#039;&#039;error&#039;&#039; geht bei &#039;&#039;status&#039;&#039; = 9 &#039;&#039;&#039;an den Konsumenten&#039;&#039;&#039;&lt;br /&gt;
und wird typischerweise direkt unter dem Passwortfeld angezeigt. Er sollte für&lt;br /&gt;
Endanwender verständlich sein und keine internen Details verraten.}}&lt;br /&gt;
&lt;br /&gt;
Liefert das Skript einen anderen Status als 1 oder 9, ist gar nicht lauffähig oder&lt;br /&gt;
gibt kein auswertbares JSON zurück, antwortet der Server mit &#039;&#039;&#039;403&#039;&#039;&#039; und einer&lt;br /&gt;
generischen Meldung - ein defektes Anmeldeskript soll dem Anwender nicht als&lt;br /&gt;
„Passwort falsch&amp;quot; erscheinen.&lt;br /&gt;
&lt;br /&gt;
Kann der Server die &#039;&#039;&#039;Sitzungszeile&#039;&#039;&#039; zum ausgestellten Token nicht schreiben&lt;br /&gt;
(Tabelle fehlt, Datenbankproblem), liefert er &#039;&#039;&#039;kein Token&#039;&#039;&#039; aus und antwortet&lt;br /&gt;
mit &#039;&#039;&#039;503&#039;&#039;&#039; und &#039;&#039;Retry-After&#039;&#039;. Auch das ist bewusst kein 401/403: die&lt;br /&gt;
Zugangsdaten waren richtig, der Versuch darf wiederholt werden.&lt;br /&gt;
&lt;br /&gt;
===Aufbau der Token-Antwort===&lt;br /&gt;
&lt;br /&gt;
Bei Erfolg baut der Server die Antwort aus den Token-Feldern &#039;&#039;&#039;und allen weiteren&lt;br /&gt;
Feldern, die das Skript zurückgibt&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;benutzerId&amp;quot;: &amp;quot;4711&amp;quot;, &amp;quot;tenant&amp;quot;: &amp;quot;nord&amp;quot;, &amp;quot;roles&amp;quot;: [&amp;quot;TECHNIKER&amp;quot;],&lt;br /&gt;
  &amp;quot;token&amp;quot;:       &amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;accessToken&amp;quot;: &amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;refreshToken&amp;quot;:&amp;quot;eyJ...&amp;quot;,&lt;br /&gt;
  &amp;quot;expiresIn&amp;quot;:   28800,&lt;br /&gt;
  &amp;quot;serverTime&amp;quot;:  &amp;quot;2026-08-17T09:12:33+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld !! Herkunft&lt;br /&gt;
|-&lt;br /&gt;
| token / accessToken || Derselbe Access-Token unter zwei Namen. &#039;&#039;accessToken&#039;&#039; erwarten die meisten Client-Bibliotheken, &#039;&#039;token&#039;&#039; bleibt für bestehende Konsumenten erhalten&lt;br /&gt;
|-&lt;br /&gt;
| refreshToken || Nur wenn das Skript &#039;&#039;_OBS_JWT_REFRESH_ID&#039;&#039; geliefert hat&lt;br /&gt;
|-&lt;br /&gt;
| expiresIn || Laufzeit des Access-Tokens in &#039;&#039;&#039;Sekunden&#039;&#039;&#039; (&#039;&#039;JWT-Exp&#039;&#039; × 60)&lt;br /&gt;
|-&lt;br /&gt;
| serverTime || Serverzeit ISO-8601 mit Offset&lt;br /&gt;
|-&lt;br /&gt;
| alle übrigen Felder || &#039;&#039;&#039;Frei vom Skript bestimmt.&#039;&#039;&#039; Übernommen wird alles ausser &#039;&#039;status&#039;&#039; und den &#039;&#039;_OBS_&#039;&#039;-Steuerfeldern&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Damit kann das Anmeldeskript alles mitliefern, was der Client direkt nach dem Login&lt;br /&gt;
braucht (Benutzer-Id, Mandant, Rollen, Berechtigungen) - ohne dass dieser dafür&lt;br /&gt;
einen zweiten Aufruf absetzen muss. Ein Feld, das genauso heisst wie eines der&lt;br /&gt;
Token-Felder oben, wird verworfen; die Engine setzt diese selbst.&lt;br /&gt;
&lt;br /&gt;
===Custom-Claims, serverTime und Refresh===&lt;br /&gt;
&lt;br /&gt;
Das Authenticate-Skript kann dem Token zusätzlich &#039;&#039;&#039;beliebige Custom-Claims&#039;&#039;&#039; mitgeben - z.B. Mandant und Rollen - und optional ein &#039;&#039;&#039;Refresh-Token&#039;&#039;&#039; anstoßen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! RÜckgabefeld !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| _OBS_JWT_CLAIM_&amp;amp;lt;name&amp;amp;gt; || Beliebiger Custom-Claim, z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles. Im Folge-Skript lesbar als &amp;lt;code&amp;gt;oReader.Claim(&#039;&amp;amp;lt;name&amp;amp;gt;&#039;)&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| _OBS_JWT_REFRESH_ID || (optional) jti des Refresh-Tokens. Nur wenn gesetzt, stellt der Server ein Refresh-Token aus.&lt;br /&gt;
|-&lt;br /&gt;
| _OBS_JWT_REFRESH_EXP || (optional) Lebensdauer des Refresh-Tokens in Minuten (Default 90 Tage = 129600).&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Jedes Token trägt automatisch den Claim &#039;&#039;token_use&#039;&#039; (&#039;&#039;access&#039;&#039; bzw. &#039;&#039;refresh&#039;&#039;) sowie die Sitzungskennungen &#039;&#039;sid&#039;&#039; und &#039;&#039;rid&#039;&#039; des Servers. Die Antwort enthält bei Erfolg &#039;&#039;serverTime&#039;&#039; (ISO-8601 mit Offset), bei ausgestelltem Refresh zusätzlich &#039;&#039;refreshToken&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
 {&amp;quot;token&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;refreshToken&amp;quot;:&amp;quot;...&amp;quot;, &amp;quot;serverTime&amp;quot;:&amp;quot;2026-06-29T15:30:12+02:00&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Mandant und Rollen immer aus dem Token lesen (&#039;&#039;oReader.Claim(…)&#039;&#039;), nie aus Body oder Query.}}&lt;br /&gt;
&lt;br /&gt;
====Refresh-Methode====&lt;br /&gt;
&lt;br /&gt;
Der Refresh läuft über &#039;&#039;&#039;denselben JWT-Endpunkt&#039;&#039;&#039; und &#039;&#039;&#039;dasselbe Skript&#039;&#039;&#039;. Liegt am JWT-Endpunkt ein &#039;&#039;Authorization: Bearer &amp;lt;token&amp;gt;&#039;&#039; mit einem Refresh-Token vor, ruft der Server statt &#039;&#039;Authenticate&#039;&#039; die Methode &#039;&#039;&#039;Refresh&#039;&#039;&#039; auf (ohne Bearer: Login; &#039;&#039;DELETE&#039;&#039; mit Access-Token: Abmelden, ohne Skript).&lt;br /&gt;
&lt;br /&gt;
Bevor das Skript läuft, hat der Server das vorgelegte Refresh-Token verifiziert&lt;br /&gt;
(Signatur, Ablauf, &#039;&#039;token_use=refresh&#039;&#039;) und in der Sitzung &#039;&#039;&#039;entwertet&#039;&#039;&#039;.&lt;br /&gt;
Einmalgebrauch, Rotation und Sperrliste sind damit Sache des Servers. Das Skript&lt;br /&gt;
liefert nur noch die aktuellen Claims - genau das ist der Sinn des Aufrufs: eine&lt;br /&gt;
zwischenzeitlich geänderte Rolle oder ein gewechselter Mandant wirken spätestens&lt;br /&gt;
mit der nächsten Erneuerung.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Eine &#039;&#039;&#039;eigene Sperrtabelle im Skript ist nicht mehr nötig&#039;&#039;&#039; und sollte&lt;br /&gt;
entfernt werden. Wer sie weiterführt, rotiert zweimal - einmal im Skript, einmal im&lt;br /&gt;
Server - und riskiert, dass beide Seiten unterschiedlicher Meinung sind. Der&lt;br /&gt;
Server sieht die Skript-Tabelle nicht, und das Skript sieht die Sitzung nicht.}}&lt;br /&gt;
&lt;br /&gt;
Ein bereits benutztes Refresh-Token wird mit &#039;&#039;&#039;401&#039;&#039;&#039; abgelehnt. Erfolgt die&lt;br /&gt;
erneute Vorlage innerhalb von 60 Sekunden, bleibt die Sitzung bestehen (paralleler&lt;br /&gt;
Refresh einer App); danach gilt sie als Wiedervorlage und die &#039;&#039;&#039;gesamte Sitzung&lt;br /&gt;
wird gesperrt&#039;&#039;&#039;. Details in [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
Das alte Refresh-jti steht weiterhin als &#039;&#039;oReader.Param(&#039;_OBS_JWT_ID&#039;)&#039;&#039; bereit,&lt;br /&gt;
falls das Skript es für eigene Protokollierung braucht.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Refresh(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUser: string;&lt;br /&gt;
begin&lt;br /&gt;
    cUser := oReader.Subject();&lt;br /&gt;
&lt;br /&gt;
    // Der Server hat das vorgelegte Refresh-Token bereits geprueft und&lt;br /&gt;
    // entwertet. Hier wird nur entschieden, ob der Benutzer die Sitzung&lt;br /&gt;
    // fortsetzen darf - und mit welchen Rechten.&lt;br /&gt;
    if (not BenutzerAktiv(cUser)) then begin&lt;br /&gt;
        oWriter.Int(&#039;status&#039;, 9);&lt;br /&gt;
        oWriter.Str(&#039;error&#039; , &#039;Konto ist nicht mehr aktiv, bitte neu anmelden&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
&lt;br /&gt;
    // Rolle und Mandant neu lesen, nicht aus dem alten Token uebernehmen -&lt;br /&gt;
    // sonst wirkt ein Rechteentzug erst beim naechsten Login.&lt;br /&gt;
    oWriter.Int(&#039;status&#039;               , 1);&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_ID&#039;          , cUser);&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_SUBJECT&#039;     , cUser);&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_CLAIM_tenant&#039;, TechnikerMandant(cUser));&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_CLAIM_roles&#039; , TechnikerRollen(cUser));&lt;br /&gt;
    oWriter.Str(&#039;_OBS_JWT_REFRESH_ID&#039;  , GlobalUID());   // loest das neue Refresh-Token aus&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Abmelden (Logout)===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Für das Abmelden braucht das Skript keine Methode.&#039;&#039;&#039; Es läuft über ein&lt;br /&gt;
&#039;&#039;DELETE&#039;&#039; auf denselben JWT-Endpunkt (Access-Token im &#039;&#039;Authorization&#039;&#039;-Header)&lt;br /&gt;
und wird komplett in der Engine erledigt: die Sitzung wird gesperrt, die Antwort&lt;br /&gt;
ist 204. Eine Methode &#039;&#039;Logout&#039;&#039; gibt es nicht und wird nicht aufgerufen.&lt;br /&gt;
Details: [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]], Phase 4.&lt;br /&gt;
&lt;br /&gt;
Wer beim Abmelden zusätzlich fachlich etwas tun will - eine Geräteregistrierung&lt;br /&gt;
löschen, einen Push-Token verwerfen, den Vorgang protokollieren - nimmt einen&lt;br /&gt;
&#039;&#039;&#039;gewöhnlichen Endpunkt&#039;&#039;&#039; und ruft dort &#039;&#039;oWriter.JwtRevoke&#039;&#039;. Dasselbe gilt für&lt;br /&gt;
&amp;quot;auf allen Geräten abmelden&amp;quot;, weil das eine Berechtigungsentscheidung braucht:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    GeraeteRegistrierungLoeschen(oReader.Subject());&lt;br /&gt;
&lt;br /&gt;
    // &#039;session&#039; = diese Sitzung, &#039;all&#039; = alle Sitzungen des Subjects&lt;br /&gt;
    oWriter.JwtRevoke(&#039;session&#039;);&lt;br /&gt;
    oWriter.Status(204);&lt;br /&gt;
    oWriter.NoBody();&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Gesperrt wird in beiden Wegen die &#039;&#039;&#039;ganze Sitzung&#039;&#039;&#039;, also auch die noch nicht&lt;br /&gt;
abgelaufenen Access-Token vorheriger Erneuerungen. Ein wiederholter Aufruf ist&lt;br /&gt;
unschädlich. Lässt sich die Sitzung nicht sperren, überschreibt der Server die&lt;br /&gt;
Antwort des Skripts mit &#039;&#039;&#039;503&#039;&#039;&#039; - ein Client darf nicht glauben, er sei&lt;br /&gt;
abgemeldet, während sein Token weiterläuft.&lt;br /&gt;
&lt;br /&gt;
==Fehlerbehandlung im Skript==&lt;br /&gt;
&lt;br /&gt;
Tritt im Skript eine Exception auf oder schlägt die Syntax-Prüfung fehl, antwortet der Server mit 500 &#039;&#039;Interner Fehler&#039;&#039; und protokolliert die Detail-Meldung in &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; (mit Skript-Fehlertext). Der Konsument sieht keine internen Details.&lt;br /&gt;
&lt;br /&gt;
Server-eigene Fehler (Routing, Rate-Limit, Token, Auffangnetz) haben ein&lt;br /&gt;
einheitliches Format mit einem &#039;&#039;error&#039;&#039;-Objekt - Aufbau siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer|Übersicht]].&lt;br /&gt;
&lt;br /&gt;
Für einen spezifischen Fehler an den Konsumenten gibt es &#039;&#039;&#039;einen&#039;&#039;&#039; Aufruf. Er&lt;br /&gt;
baut dieselbe Hülle, die auch die Server-eigenen Fehler tragen - Statuscode,&lt;br /&gt;
&#039;&#039;code&#039;&#039;, &#039;&#039;message&#039;&#039;, &#039;&#039;uid&#039;&#039; und &#039;&#039;traceId&#039;&#039; -, sodass beide Wege nicht&lt;br /&gt;
auseinanderlaufen können:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Post(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
begin&lt;br /&gt;
    if (Empty(oReader.Param(&#039;kundennr&#039;))) then begin&lt;br /&gt;
        oWriter.Error(422, &#039;VALIDATION_FAILED&#039;, &#039;Parameter &amp;quot;kundennr&amp;quot; fehlt&#039;);&lt;br /&gt;
        exit;&lt;br /&gt;
    end;&lt;br /&gt;
    // ...&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Error verwirft einen schon begonnenen Körper&#039;&#039;&#039; und setzt seinen&lt;br /&gt;
eigenen. Ein Skript darf also getrost Felder schreiben und danach noch&lt;br /&gt;
abbrechen - was der Client sieht, ist die Fehlerantwort. Nur ein &#039;&#039;&#039;offener&lt;br /&gt;
Stapel&#039;&#039;&#039; (ein &#039;&#039;ObjBegin&#039;&#039; ohne &#039;&#039;ObjEnd&#039;&#039;) bleibt auch dann ein Strukturfehler.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein fachlicher Fehler gehört auf einen &#039;&#039;&#039;4xx&#039;&#039;&#039;-Statuscode, nicht auf&lt;br /&gt;
200 mit Fehlertext im Body. Nur so erkennt der Konsument den Fehlschlag ohne den&lt;br /&gt;
Body auszuwerten - und nur so gibt der Server einen belegten&lt;br /&gt;
&#039;&#039;Idempotency-Key&#039;&#039; wieder frei (siehe Abschnitt Idempotenz).}}&lt;br /&gt;
&lt;br /&gt;
===Ändern: qSqlInit statt qSqlRead===&lt;br /&gt;
&lt;br /&gt;
Zum &#039;&#039;&#039;Ändern&#039;&#039;&#039; eines vorhandenen Satzes wird ebenfalls &amp;lt;code&amp;gt;qSqlInit&amp;lt;/code&amp;gt;&lt;br /&gt;
benutzt - der Satz wird über &amp;lt;code&amp;gt;sys_uid&amp;lt;/code&amp;gt; adressiert:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
q := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
q.qSet(&#039;sys_uid&#039;  , cSysUid);      // Schreib-Index auf den Originalsatz&lt;br /&gt;
q.qSet(&#039;ti_status&#039;, &#039;9&#039;);&lt;br /&gt;
if (not q.SaveData(UPDATE_RECORD)) then begin&lt;br /&gt;
    // ... Fehler behandeln, siehe unten&lt;br /&gt;
end;&lt;br /&gt;
qSqlFree(q);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;qSqlRead&amp;lt;/code&amp;gt; ist dafür die falsche Wahl, aus zwei Gründen:&lt;br /&gt;
&lt;br /&gt;
* Es liest den &#039;&#039;&#039;kompletten Altsatz&#039;&#039;&#039; ein - ein zusätzlicher Lesezugriff, der Zeit kostet.&lt;br /&gt;
* Es schreibt anschliessend nur die &#039;&#039;&#039;Änderungen&#039;&#039;&#039;. Ist der neue Wert gleich dem alten, entsteht gar kein Write, und &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt; liefert &#039;&#039;&#039;False&#039;&#039;&#039; - ununterscheidbar von einem echten Fehlschlag.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ohne &amp;lt;code&amp;gt;sys_uid&amp;lt;/code&amp;gt; legt &amp;lt;code&amp;gt;qSqlInit&amp;lt;/code&amp;gt; einen &#039;&#039;&#039;neuen&#039;&#039;&#039;&lt;br /&gt;
Datensatz an, statt den vorhandenen zu ändern. Die &amp;lt;code&amp;gt;sys_uid&amp;lt;/code&amp;gt; ist der&lt;br /&gt;
Schreib-Index und gehört bei jeder Änderung gesetzt.}}&lt;br /&gt;
&lt;br /&gt;
Damit entfällt auch das übliche Paar aus &amp;lt;code&amp;gt;DB_LSeek&amp;lt;/code&amp;gt; und&lt;br /&gt;
&amp;lt;code&amp;gt;qSqlRead&amp;lt;/code&amp;gt;: &#039;&#039;&#039;eine&#039;&#039;&#039; Abfrage auf die &amp;lt;code&amp;gt;sys_uid&amp;lt;/code&amp;gt; liefert&lt;br /&gt;
zugleich die Existenzprüfung und den Schreib-Index.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
cUid := &#039;&#039;;&lt;br /&gt;
if (DB_SOpen(oDB, &#039;SELECT sys_uid FROM tickets WHERE &#039; + cWhere + &#039; LIMIT 1&#039;, q)) then begin&lt;br /&gt;
    if (not q.EoF) then begin&lt;br /&gt;
        cUid := q.A2UID();&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
DB_Close(q);&lt;br /&gt;
&lt;br /&gt;
if (not Empty(cUid)) then begin&lt;br /&gt;
    // ändern: qSqlInit + sys_uid&lt;br /&gt;
end else begin&lt;br /&gt;
    // anlegen: qSqlInit ohne sys_uid&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Mehrere Felder sind EIN Schreibvorgang===&lt;br /&gt;
&lt;br /&gt;
Ein &amp;lt;code&amp;gt;qSqlInit&amp;lt;/code&amp;gt;-Objekt nimmt beliebig viele &amp;lt;code&amp;gt;qSet&amp;lt;/code&amp;gt; entgegen&lt;br /&gt;
und schreibt sie mit &#039;&#039;&#039;einem&#039;&#039;&#039; &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt;. Fallen an derselben&lt;br /&gt;
Zeile mehrere Felder an, gehören sie in dasselbe Objekt:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
// FALSCH: fünf Felder, fünf Schreibvorgänge - und jeder Setzer, der sich&lt;br /&gt;
// seine sys_uid selbst holt, bringt zusätzlich eine eigene Abfrage mit&lt;br /&gt;
FeldSetzen(cUid, &#039;ti_status&#039; , &#039;9&#039;);&lt;br /&gt;
FeldSetzen(cUid, &#039;ti_grund&#039;  , cGrund);&lt;br /&gt;
FeldSetzen(cUid, &#039;ti_von&#039;    , cBenutzer);&lt;br /&gt;
&lt;br /&gt;
// RICHTIG: ein Schreibobjekt, ein SaveData&lt;br /&gt;
q := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
q.qSet(&#039;sys_uid&#039;  , cUid);&lt;br /&gt;
q.qSet(&#039;ti_status&#039;, &#039;9&#039;);&lt;br /&gt;
q.qSet(&#039;ti_grund&#039; , cGrund);&lt;br /&gt;
q.qSet(&#039;ti_von&#039;   , cBenutzer);&lt;br /&gt;
if (not q.SaveData(UPDATE_RECORD)) then begin&lt;br /&gt;
    // ... Fehler behandeln&lt;br /&gt;
end;&lt;br /&gt;
qSqlFree(q);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Das ist nicht nur eine Frage der Geschwindigkeit: einzeln geschriebene Felder&lt;br /&gt;
sind &#039;&#039;&#039;nicht atomar&#039;&#039;&#039;. Bricht der dritte Schreibvorgang ab, steht die Zeile&lt;br /&gt;
halb geändert da - Grund gesetzt, Zeitpunkt nicht.&lt;br /&gt;
&lt;br /&gt;
Wird nichts gesetzt, darf auch nicht geschrieben werden: das Schreibobjekt dann&lt;br /&gt;
mit &amp;lt;code&amp;gt;qSqlFree&amp;lt;/code&amp;gt; freigeben, statt ein leeres &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt;&lt;br /&gt;
abzusetzen.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein Setzer für &#039;&#039;&#039;ein&#039;&#039;&#039; Feld (&amp;lt;code&amp;gt;FeldSetzen(cUid, cFeld, cWert)&amp;lt;/code&amp;gt;)&lt;br /&gt;
ist bequem und deshalb verführerisch. Er versteckt aber je Aufruf eine eigene&lt;br /&gt;
Abfrage und ein eigenes &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt;. Für einen Vorgang mit mehreren&lt;br /&gt;
Feldern gehört ein &#039;&#039;&#039;Editier-Helfer&#039;&#039;&#039; her, der das Schreibobjekt liefert -&lt;br /&gt;
geschrieben wird einmal am Ende.}}&lt;br /&gt;
&lt;br /&gt;
===Keine Abfrage je Zeile===&lt;br /&gt;
&lt;br /&gt;
Was in einer Liste je Zeile nachgeschlagen wird, kostet &#039;&#039;&#039;N&#039;&#039;&#039; Abfragen für&lt;br /&gt;
&#039;&#039;&#039;eine&#039;&#039;&#039; Antwort. Bei fünfzig Aufträgen sind das fünfzig Abfragen für ein&lt;br /&gt;
einziges Feld. Der Ausweg ist immer derselbe:&lt;br /&gt;
&lt;br /&gt;
* Werte aus einer 1:1-Beziehung über einen &amp;lt;code&amp;gt;LEFT JOIN&amp;lt;/code&amp;gt; mitnehmen - bei einem &#039;&#039;&#039;eindeutigen&#039;&#039;&#039; Index auf dem Join-Feld kann er die Zeilen nicht vervielfachen.&lt;br /&gt;
* Einzelwerte aus einer 1:n-Beziehung über eine &#039;&#039;&#039;Unterabfrage&#039;&#039;&#039; in der SELECT-Liste holen, nicht über einen Join (der Join würde die Zeilen vervielfachen).&lt;br /&gt;
* Eine echte Unterliste (Positionen, Dateien) nur dann nachladen, wenn ein &#039;&#039;&#039;Zählfeld&#039;&#039;&#039; in der Hauptabfrage sagt, dass es überhaupt eine gibt.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein GET soll &#039;&#039;&#039;lesen&#039;&#039;&#039;. Schreibvorgänge im Lesepfad - etwas&lt;br /&gt;
&amp;quot;beim ersten Ausliefern festschreiben&amp;quot; - vervielfachen sich mit der Zeilenzahl&lt;br /&gt;
und treffen den Aufrufer, der nur eine Liste angefordert hat.}}&lt;br /&gt;
&lt;br /&gt;
===Schreibfehler erkennen===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;&amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt; liefert einen Boolean - und der ist die einzige&lt;br /&gt;
Fehlermeldung, die es gibt.&#039;&#039;&#039; (Auch der &#039;&#039;&#039;Parameter&#039;&#039;&#039; ist ein Boolean:&lt;br /&gt;
&#039;&#039;NEW_RECORD&#039;&#039; und &#039;&#039;UPDATE_RECORD&#039;&#039; sind Wahrheitswerte, keine Zahlen.) Schlägt das Schreiben in der Datenbank fehl&lt;br /&gt;
(&#039;&#039;Duplicate entry&#039;&#039; auf einem eindeutigen Index, zu langer Wert, fehlende&lt;br /&gt;
Spalte), dann&lt;br /&gt;
&lt;br /&gt;
* wird &#039;&#039;&#039;keine&#039;&#039;&#039; Exception ausgelöst,&lt;br /&gt;
* steht &#039;&#039;&#039;nichts&#039;&#039;&#039; in &#039;&#039;RESTSRV_PROTO&#039;&#039;,&lt;br /&gt;
* läuft das Skript weiter und antwortet mit dem Erfolg, den es selbst formuliert.&lt;br /&gt;
&lt;br /&gt;
Wer den Aufruf als Anweisung schreibt, baut damit einen stillen Datenverlust ein.&lt;br /&gt;
Weil in einem Endpunkt schnell ein Dutzend Schreibvorgänge zusammenkommen, gehört&lt;br /&gt;
die Prüfung in eine eigene kleine Prozedur - am besten in die gemeinsame Lib:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot; line&amp;gt;&lt;br /&gt;
procedure Speichern(q: TqSQL; lNeu: Boolean; const cWas: string);&lt;br /&gt;
var lOk: Boolean;&lt;br /&gt;
begin&lt;br /&gt;
    lOk := q.SaveData(lNeu);&lt;br /&gt;
    qSqlFree(q);                     // auch im Fehlerfall freigeben&lt;br /&gt;
    if (not lOk) then begin&lt;br /&gt;
        raise Exception.Create(&#039;Schreiben fehlgeschlagen: &#039; + cWas);&lt;br /&gt;
    end;&lt;br /&gt;
end;&lt;br /&gt;
&lt;br /&gt;
// Aufruf statt SaveData + qSqlFree:&lt;br /&gt;
q := qSqlInit(oDB, &#039;tickets&#039;);&lt;br /&gt;
q.qSet(&#039;ti_betreff&#039;, cBetreff);&lt;br /&gt;
Speichern(q, NEW_RECORD, &#039;Ticket&#039;);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Exception fängt der Server: er schreibt &#039;&#039;Schreiben fehlgeschlagen: Ticket&#039;&#039;&lt;br /&gt;
nach &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; und antwortet mit &#039;&#039;&#039;500&#039;&#039;&#039;. Liegt ein&lt;br /&gt;
&#039;&#039;Idempotency-Key&#039;&#039; an, wird er dabei &#039;&#039;&#039;freigegeben&#039;&#039;&#039; - die Anfrage ist also&lt;br /&gt;
wiederholbar (siehe Abschnitt Idempotenz).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein fehlgeschlagenes Schreiben ist kein Fall, in dem ein Endpunkt&lt;br /&gt;
sinnvoll weiterlaufen kann: die Fachwirkung ist dann unvollständig. Wer trotzdem&lt;br /&gt;
weitermachen will - etwa weil mehrere unabhängige Sätze geschrieben werden -,&lt;br /&gt;
prüft den Rückgabewert und baut selbst eine Antwort. Ignorieren darf man ihn&lt;br /&gt;
nie.}}&lt;br /&gt;
&lt;br /&gt;
Gängige Codes, die auch der Server selbst verwendet: &#039;&#039;VALIDATION_FAILED&#039;&#039; (422),&lt;br /&gt;
&#039;&#039;NOT_FOUND&#039;&#039; (404), &#039;&#039;FORBIDDEN_ROLE&#039;&#039; (403), &#039;&#039;VERSION_CONFLICT&#039;&#039; (409).&lt;br /&gt;
Eigene, fachlich sprechende Codes sind erlaubt - wichtig ist, dass sie stabil&lt;br /&gt;
bleiben, weil Konsumenten darauf ihre Reaktion aufbauen.&lt;br /&gt;
&lt;br /&gt;
==Gemeinsamen Code auslagern==&lt;br /&gt;
&lt;br /&gt;
Wird Logik in mehreren Endpunkten gebraucht - Berechtigungsprüfungen,&lt;br /&gt;
JSON-Bausteine, Hilfsfunktionen -, muss sie nicht in jedes Skript kopiert werden.&lt;br /&gt;
Die Skript-Engine lädt Textbausteine beim Laden aus einer beliebigen Tabelle nach:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
{$L T=&amp;quot;restsrv_endpoints&amp;quot; I=&amp;quot;re_pathtemplate&amp;quot; V=&amp;quot;/meinapp/lib&amp;quot; F=&amp;quot;re_script&amp;quot;}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Attribut !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;T&#039;&#039;&#039; || Tabelle, aus der geladen wird&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;I&#039;&#039;&#039; || Spalte, über die gesucht wird&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;V&#039;&#039;&#039; || Wert, der in dieser Spalte stehen muss&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;F&#039;&#039;&#039; || Spalte, deren Inhalt an dieser Stelle eingesetzt wird&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die Direktive steht üblicherweise direkt unter dem Kopfkommentar des Skripts. Der&lt;br /&gt;
Compiler sieht danach den zusammengesetzten Text - eine Funktion aus der Lib lässt&lt;br /&gt;
sich also aufrufen wie eine im Skript selbst geschriebene.&lt;br /&gt;
&lt;br /&gt;
===Bewährtes Muster: Lib als eigene Endpunkt-Zeile===&lt;br /&gt;
&lt;br /&gt;
Am wenigsten Verwaltung macht es, die Lib &#039;&#039;&#039;als eigene Zeile in&lt;br /&gt;
RESTSRV_ENDPOINTS&#039;&#039;&#039; abzulegen:&lt;br /&gt;
&lt;br /&gt;
# Endpunkt anlegen mit einem eigenen Pfad-Template, z.B. &amp;lt;code&amp;gt;/meinapp/lib&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Den Lib-Quelltext ins Skript-Feld dieser Zeile schreiben (F7).&lt;br /&gt;
# &#039;&#039;&#039;Keine Berechtigung&#039;&#039;&#039; in der Berechtigungs-Liste vergeben - das ist der Schutz: ohne Berechtigung beantwortet der Server einen Aufruf mit 403.&lt;br /&gt;
# In jedem nutzenden Endpunkt die Direktive oben einfügen.&lt;br /&gt;
&lt;br /&gt;
Damit ist die Lib reiner Ablageort und von aussen nicht nutzbar. Eine Änderung&lt;br /&gt;
wirkt auf alle einbindenden Endpunkte, ohne dass ein einziges Endpunkt-Skript&lt;br /&gt;
angefasst werden muss.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Lib und Skripte gemeinsam einspielen.&#039;&#039;&#039; Kommt in der Lib eine neue&lt;br /&gt;
Funktion hinzu, genügt es nicht, nur das nutzende Skript zu aktualisieren. Eine&lt;br /&gt;
ältere Lib lädt fehlerfrei - der Compiler meldet dann ausschliesslich die neuen&lt;br /&gt;
Namen als &#039;&#039;Unknown name&#039;&#039;, was leicht wie ein Fehler im Skript aussieht.}}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Auch hier greift der Endpunkt-Cache: eine Änderung an der Lib wird erst&lt;br /&gt;
nach Ablauf des TTL (bis zu 60 Sekunden) wirksam.}}&lt;br /&gt;
&lt;br /&gt;
===Wohin gehört welcher Code?===&lt;br /&gt;
&lt;br /&gt;
Drei Ebenen, und die Zuordnung entscheidet, wie schnell eine Änderung wirkt:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Art des Codes !! Wohin !! Wirksam&lt;br /&gt;
|-&lt;br /&gt;
| Fachlogik **einer** Anwendung - Feldnamen, Zustandswerte, Rollenregeln, Schreibziele || in die &#039;&#039;&#039;DWS-Lib dieser Anwendung&#039;&#039;&#039; (eigene Endpunkt-Zeile, per &amp;lt;code&amp;gt;{$L …}&amp;lt;/code&amp;gt; eingebunden) || nach Ablauf des Cache-TTL, ohne Build&lt;br /&gt;
|-&lt;br /&gt;
| Mechanik, die &#039;&#039;&#039;mehrere&#039;&#039;&#039; Anwendungen brauchen - und auch das OBS-Umfeld || nach &#039;&#039;&#039;obs_lib&#039;&#039;&#039; als Delphi-Unit || erst nach Build und Rollout&lt;br /&gt;
|-&lt;br /&gt;
| Mechanik, die &#039;&#039;&#039;nur&#039;&#039;&#039; der REST-Server braucht || in die Units des Servers || erst nach Build und Rollout&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die Regel hat einen praktischen Grund und keinen ästhetischen: Fachlogik ändert&lt;br /&gt;
sich in Tagen, Mechanik in Monaten. Was in der Lib liegt, ist nach einer Minute&lt;br /&gt;
aktiv; was in einer Unit liegt, braucht einen Build. Wer Fachlogik nach unten&lt;br /&gt;
schiebt, tauscht Geschwindigkeit gegen nichts ein.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Eine Anwendung, die ihre eigene Lib hat, kopiert sie &#039;&#039;&#039;nicht&#039;&#039;&#039; von&lt;br /&gt;
einer anderen. Zwei Kopien derselben Lib driften auseinander, und die Abweichung&lt;br /&gt;
fällt erst auf, wenn eine der beiden Anwendungen etwas Falsches tut. Geteiltes&lt;br /&gt;
gehört eine Ebene tiefer.}}&lt;br /&gt;
&lt;br /&gt;
==Fallstricke im Skript==&lt;br /&gt;
&lt;br /&gt;
Die folgenden Punkte unterscheiden das Skript von normalem Delphi-Code und kosten&lt;br /&gt;
sonst eine Runde Fehlersuche:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Stolperstein !! Richtig&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;DB_SOpen(&#039;Y00…&#039;, oDB, …)&#039;&#039; - der AD-UID-Parameter aus dem Delphi-Code || Im Skript &#039;&#039;&#039;ohne UID&#039;&#039;&#039;: &amp;lt;code&amp;gt;DB_SOpen(oDB, cSql, q)&amp;lt;/code&amp;gt;. Gleiches gilt für &amp;lt;code&amp;gt;qSqlInit(oDB, &#039;tab&#039;)&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;qSqlRead(oDB, &#039;tab&#039;, cWhere)&amp;lt;/code&amp;gt; - jede dieser Funktionen beginnt mit &#039;&#039;oDB&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;oReader.Str&amp;amp;lt;T&amp;amp;gt;(…)&#039;&#039; oder irgendein Generic || Generics gibt es in der Skriptsprache nicht. Die Leser sind je Typ eigene Methoden: &#039;&#039;Str&#039;&#039;, &#039;&#039;Int&#039;&#039;, &#039;&#039;Num&#039;&#039;, &#039;&#039;Bool&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Eine eigene Prozedur &#039;&#039;Delete&#039;&#039; für DELETE || &#039;&#039;Delete&#039;&#039; ist eine &#039;&#039;&#039;Standardprozedur&#039;&#039;&#039; der Sprache. Die Deklaration lässt sich nicht übersetzen, und die Meldung nennt nur die Zeile - nicht die Ursache&lt;br /&gt;
|-&lt;br /&gt;
| Ein Helfer, der ein JSON-Fragment als String zurückgibt || Den &#039;&#039;&#039;Writer als Parameter&#039;&#039;&#039; übergeben und an der richtigen Stelle hineinschreiben. Fragmente von Hand zusammenzusetzen ist genau der Weg, auf dem die Reihenfolge und die Klammern verloren gingen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;oWriter.ArrEnd&#039;&#039; nach &#039;&#039;RootArr&#039;&#039; || &#039;&#039;RootArr&#039;&#039; öffnet &#039;&#039;&#039;keine&#039;&#039;&#039; Ebene, es bestimmt nur die Klammer der Wurzel. Ein &#039;&#039;ArrEnd&#039;&#039; dazu ist ein Strukturfehler&lt;br /&gt;
|-&lt;br /&gt;
| Zahlen selbst formatieren (&#039;&#039;NStr&#039;&#039;, &#039;&#039;StrTran&#039;&#039;) || &amp;lt;code&amp;gt;oWriter.Num(&#039;preis&#039;, nWert, 2)&amp;lt;/code&amp;gt;. Selbst formatiert stand bei Werten ab 1000 der Tausendertrenner im JSON - gültiges JSON, falscher Wert, keine Meldung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;EMPTY_DATE&#039;&#039; für ein leeres Datum || &#039;&#039;EMPTY_DATE&#039;&#039; ist im Skript ein &#039;&#039;&#039;String&#039;&#039;&#039;. Für Datumsvariablen und -vergleiche &#039;&#039;&#039;MINDATETIME&#039;&#039;&#039; verwenden&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;fVal&#039;&#039; auf einen JSON-Zahlenwert || JSON liefert den Dezimalpunkt, &#039;&#039;fVal&#039;&#039; erwartet die lokale Notation: vorher &amp;lt;code&amp;gt;StrTran(cVal, &#039;.&#039;, &#039;,&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;TStringList&#039;&#039; für Zwischenlisten || In Skripten unzuverlässig. Kleine Mengen über einen Delimiter-String führen (&amp;lt;code&amp;gt;&#039;&amp;amp;#124;&#039; + wert + &#039;&amp;amp;#124;&#039;&amp;lt;/code&amp;gt;) und mit &#039;&#039;Pos&#039;&#039; prüfen&lt;br /&gt;
|-&lt;br /&gt;
| Default-Parameter in eigenen Funktionen || Werden nicht unterstützt - alle Parameter ausschreiben&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;q.SaveData(…)&amp;lt;/code&amp;gt; als Anweisung schreiben || &#039;&#039;SaveData&#039;&#039; liefert einen &#039;&#039;&#039;Boolean&#039;&#039;&#039;. Ein SQL-Fehler beim Schreiben wirft &#039;&#039;&#039;keine&#039;&#039;&#039; Exception und landet in &#039;&#039;&#039;keinem&#039;&#039;&#039; Protokoll - der Rückgabewert ist die einzige Meldung. Immer auswerten, siehe [[#Schreibfehler erkennen|Schreibfehler erkennen]]&lt;br /&gt;
|-&lt;br /&gt;
| Je Feld ein eigener Setzer-Aufruf || Ein &amp;lt;code&amp;gt;qSqlInit&amp;lt;/code&amp;gt;-Objekt, beliebig viele &amp;lt;code&amp;gt;qSet&amp;lt;/code&amp;gt;, &#039;&#039;&#039;ein&#039;&#039;&#039; &amp;lt;code&amp;gt;SaveData&amp;lt;/code&amp;gt;. Einzeln geschrieben kostet jedes Feld eine eigene Abfrage samt eigenem Write - und der Vorgang ist nicht mehr atomar, siehe [[#Mehrere Felder sind EIN Schreibvorgang|Mehrere Felder]]&lt;br /&gt;
|-&lt;br /&gt;
| Ein Wert je Zeile nachgeschlagen || In einer Liste sind das N Abfragen für eine Antwort. Über &amp;lt;code&amp;gt;LEFT JOIN&amp;lt;/code&amp;gt; (1:1) oder Unterabfrage (1:n) mitnehmen, siehe [[#Keine Abfrage je Zeile|Keine Abfrage je Zeile]]&lt;br /&gt;
|-&lt;br /&gt;
| Ausgabeparameter vom Typ &amp;lt;code&amp;gt;var … : TDateTime&amp;lt;/code&amp;gt; || Bringt die Skript-VM zum &#039;&#039;&#039;Absturz&#039;&#039;&#039; (Assertion in &#039;&#039;dwsStack.pas&#039;&#039;), und zwar beim ersten &#039;&#039;&#039;Lesen&#039;&#039;&#039; des Werts - nicht schon beim Schreiben, die Ursache liegt also nicht dort, wo der Fehler auffällt. &amp;lt;code&amp;gt;var string&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;var Integer&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;var Double&amp;lt;/code&amp;gt; laufen. Zeitpunkte als ISO-Zeichenkette zurückgeben und erst beim Aufrufer wandeln&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Skript-Cache==&lt;br /&gt;
&lt;br /&gt;
Der Server cached das kompilierte Skript pro Endpunkt (Schlüssel: &#039;&#039;sys_date&#039;&#039; des Endpunkts). Zusätzlich werden die Endpunkt-Definitionen pro Server-Profil in einem TTL-Cache (Standard 60 s) gehalten. Eine Skript-Änderung über F7 wird daher erst nach Ablauf dieses TTL (bzw. nach einer Cache-Invalidierung) wirksam - typischerweise innerhalb einer Minute, nicht zwingend sofort.&lt;br /&gt;
&lt;br /&gt;
==Verfügbare Bibliotheken==&lt;br /&gt;
&lt;br /&gt;
Im Skript können alle OBS-Standard-Bibliotheken verwendet werden. Typische Einstiegspunkte:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;Base.Tools&#039;&#039; - String-, Datum-, IIF-, Empty-Helper&lt;br /&gt;
* &#039;&#039;Base.DB&#039;&#039; / &#039;&#039;Base.xQuery&#039;&#039; - Datenbank-Operationen&lt;br /&gt;
* &#039;&#039;Base.qSqlReg&#039;&#039; - Schreibzugriffe&lt;br /&gt;
* &#039;&#039;lib_ScriptIO&#039;&#039; - Writer und Reader, siehe unten. &#039;&#039;&#039;System.JSON braucht ein Endpunkt-Skript nicht&#039;&#039;&#039; - der Writer erzeugt die Antwort, der Reader liest den Körper&lt;br /&gt;
* &#039;&#039;Base.ToolsConst&#039;&#039; - Konstanten wie &#039;&#039;CRLF&#039;&#039;, &#039;&#039;SINGELQUOTE&#039;&#039;, &#039;&#039;MINDATETIME&#039;&#039;&lt;br /&gt;
* &#039;&#039;lib_ScriptIO&#039;&#039; - &#039;&#039;TxRestWriter&#039;&#039;, &#039;&#039;TxRestReader&#039;&#039; und die Zeitfunktionen &#039;&#039;RestIsoFromDate&#039;&#039; / &#039;&#039;RestDateFromIso&#039;&#039; / &#039;&#039;RestOffsetToMinutes&#039;&#039;. Writer und Reader bekommt das Skript als Parameter; die Zeitfunktionen ruft es direkt&lt;br /&gt;
&lt;br /&gt;
Die wichtigsten Aufrufe mit ihren Skript-Signaturen:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Zweck !! Aufruf&lt;br /&gt;
|-&lt;br /&gt;
| Lesen || &amp;lt;code&amp;gt;DB_SOpen(oDB, cSql, q)&amp;lt;/code&amp;gt;, dann &amp;lt;code&amp;gt;q.EoF&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.Next()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.A2C(&#039;feld&#039;)&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;A2I&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;A2F&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;A2D&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;A2UID()&amp;lt;/code&amp;gt;, am Ende &amp;lt;code&amp;gt;DB_Close(q)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Neu anlegen || &amp;lt;code&amp;gt;q := qSqlInit(oDB, &#039;tab&#039;)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.qSet(&#039;feld&#039;, wert)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;if (not q.SaveData(NEW_RECORD)) then …&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;qSqlFree(q)&amp;lt;/code&amp;gt; – der Rückgabewert ist Pflicht, siehe [[#Schreibfehler erkennen|Schreibfehler erkennen]]&lt;br /&gt;
|-&lt;br /&gt;
| Ändern || &amp;lt;code&amp;gt;q := qSqlInit(oDB, &#039;tab&#039;)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.qSet(&#039;sys_uid&#039;, cUid)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;q.qSet(…)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;if (not q.SaveData(UPDATE_RECORD)) then …&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;qSqlFree(q)&amp;lt;/code&amp;gt; – &#039;&#039;&#039;nicht&#039;&#039;&#039; &amp;lt;code&amp;gt;qSqlRead&amp;lt;/code&amp;gt;, siehe [[#Ändern: qSqlInit statt qSqlRead|Ändern]]. Alle Felder derselben Zeile in &#039;&#039;&#039;ein&#039;&#039;&#039; Objekt, siehe [[#Mehrere Felder sind EIN Schreibvorgang|Mehrere Felder]]&lt;br /&gt;
|-&lt;br /&gt;
| Direkt ausführen || &amp;lt;code&amp;gt;DB_SqlExec(oDB, cSql)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Existenzprüfung || &amp;lt;code&amp;gt;DB_LSeek(oDB, &#039;tab&#039;, cWhere)&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Werte quoten (Pflicht) || &amp;lt;code&amp;gt;DB_SQLVal(wert)&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Alle diese Funktionen beginnen im Skript mit &#039;&#039;oDB&#039;&#039;. Ein zusätzlicher&lt;br /&gt;
AD-UID-Parameter als erstes Argument gehört zur Delphi-Variante und lässt sich im&lt;br /&gt;
Skript nicht übersetzen.}}&lt;br /&gt;
&lt;br /&gt;
Konkrete Beispiele:&lt;br /&gt;
&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel1|Beispiel 1: Daten-Abruf mit JWT]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel2|Beispiel 2: Zeiterfassung als komplette Webanwendung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel3|Beispiel 3: Datensatz anlegen mit JSON-Body (CRUD)]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4: Pfad-Parameter im Routing]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel5|Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6: Datei-Upload und Ablage im DMS]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Endpunkte&amp;diff=65542</id>
		<title>OBS/Kostenpflichtige Module/RESTServer/Endpunkte</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer/Endpunkte&amp;diff=65542"/>
		<updated>2026-09-25T09:28:32Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Idempotency-Key als Pflicht, Aufraeumen offener Reservierungen berichtigt, Fehlercodes 400/503/308 ergaenzt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
&lt;br /&gt;
= Endpunkte =&lt;br /&gt;
&lt;br /&gt;
Ein &#039;&#039;&#039;Endpunkt&#039;&#039;&#039; stellt eine Adresse bereit, über die der REST-Server konkrete&lt;br /&gt;
Funktionen anbietet. Jeder Endpunkt wird durch ein OBS-Skript oder einen WebHook&lt;br /&gt;
realisiert und ist genau einem Server-Profil zugeordnet.&lt;br /&gt;
&lt;br /&gt;
== Routing über Pfad-Templates ==&lt;br /&gt;
&lt;br /&gt;
Das Routing erfolgt über die Spalte &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; (&amp;lt;code&amp;gt;re_pathtemplate&amp;lt;/code&amp;gt;).&lt;br /&gt;
Ein Template beschreibt den vollständigen Pfad nach dem Host und kann&lt;br /&gt;
&#039;&#039;&#039;Platzhalter&#039;&#039;&#039; enthalten:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Statische Segmente&#039;&#039;&#039; müssen exakt übereinstimmen (Groß-/Kleinschreibung wird ignoriert).&lt;br /&gt;
* &#039;&#039;&#039;Platzhalter&#039;&#039;&#039; in geschweiften Klammern – z.&amp;amp;nbsp;B. &amp;lt;code&amp;gt;{uid}&amp;lt;/code&amp;gt; – passen auf einen beliebigen Wert und werden als [[#Pfad-Parameter im Skript|Pfad-Parameter]] erfasst.&lt;br /&gt;
&lt;br /&gt;
Beispiele für gültige Templates:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Template !! Passt auf !! Pfad-Parameter&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/orders&amp;lt;/code&amp;gt; || –&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/orders/4711&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;uid = 4711&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/orders/{uid}/modules/{code}&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/orders/4711/modules/A1&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;uid = 4711&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;code = A1&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/kalender/v1&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/kalender/v1&amp;lt;/code&amp;gt; || –&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Präzedenz bei mehreren Treffern ===&lt;br /&gt;
&lt;br /&gt;
Passen mehrere Templates auf denselben Pfad, &#039;&#039;&#039;gewinnt das spezifischste&#039;&#039;&#039; –&lt;br /&gt;
also das mit den meisten statischen Segmenten. So schlägt &amp;lt;code&amp;gt;/orders/summary&amp;lt;/code&amp;gt;&lt;br /&gt;
das Template &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt;, während &amp;lt;code&amp;gt;/orders/4711&amp;lt;/code&amp;gt; auf&lt;br /&gt;
&amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt; matcht.&lt;br /&gt;
&lt;br /&gt;
== Hauptfelder eines Endpunkts ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Feld !! Spalte !! Zweck&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_pathtemplate&amp;lt;/code&amp;gt; || &#039;&#039;&#039;Maßgeblich fürs Routing.&#039;&#039;&#039; Vollständiger Pfad mit Platzhaltern, z.&amp;amp;nbsp;B. &amp;lt;code&amp;gt;/orders/{uid}/modules/{code}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Server&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_server&amp;lt;/code&amp;gt; || Zuordnung zum Server-Profil (erforderlich)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Aktiv&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_aktiv&amp;lt;/code&amp;gt; || Statusflag; inaktive Endpunkte liefern 404&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Skript&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_script&amp;lt;/code&amp;gt; || DwScript-Quelltext des Handlers (siehe [[/OBS/Kostenpflichtige_Module/RESTServer/Scripting|Scripting]])&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;WebHook&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_webhook&amp;lt;/code&amp;gt; || optional: Verweis auf einen Einmal-Endpunkt statt Skript&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Info&#039;&#039;&#039; || – || Dokumentations-Freitext&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Datei-Upload&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_upload&amp;lt;/code&amp;gt; || Erlaubt Datei-Uploads an diesem Endpunkt (&amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt; = aus, &amp;lt;code&amp;gt;1&amp;lt;/code&amp;gt; = an). Ohne Freigabe werden Upload-Anfragen mit 415 abgelehnt.&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Max. Upload-Grösse&#039;&#039;&#039; || &amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt; || Maximale Dateigrösse in MB. Leer/0 = Standard 25&amp;amp;nbsp;MB. Überschreitung wird mit 413 abgelehnt.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Pfad-Parameter im Skript ==&lt;br /&gt;
&lt;br /&gt;
Die aus den Platzhaltern erfassten Werte liest das Endpunkt-Skript über&lt;br /&gt;
&amp;lt;code&amp;gt;oReader.Path&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;pascal&amp;quot;&amp;gt;&lt;br /&gt;
procedure Get(oReader: TxRestReader; oWriter: TxRestWriter);&lt;br /&gt;
var cUid: string;&lt;br /&gt;
begin&lt;br /&gt;
    cUid := oReader.Path(&#039;uid&#039;);   // aus /orders/{uid}&lt;br /&gt;
    // ...&lt;br /&gt;
end;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Werte stammen aus dem Routing und können nicht durch den Client&lt;br /&gt;
überschrieben werden.&lt;br /&gt;
&lt;br /&gt;
== Datei-Uploads ==&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt nimmt Datei-Uploads nur entgegen, wenn er dafür freigeschaltet ist&lt;br /&gt;
(&amp;lt;code&amp;gt;re_upload = 1&amp;lt;/code&amp;gt;). Andernfalls werden Upload-Anfragen mit&lt;br /&gt;
&#039;&#039;&#039;415 Unsupported Media Type&#039;&#039;&#039; abgelehnt und das Skript wird nicht ausgeführt.&lt;br /&gt;
&lt;br /&gt;
Die maximal zulässige Dateigrösse wird pro Endpunkt über &amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;&lt;br /&gt;
(in MB) festgelegt; ohne Angabe gilt der Standard von &#039;&#039;&#039;25&amp;amp;nbsp;MB&#039;&#039;&#039;. Wird das&lt;br /&gt;
Limit überschritten, antwortet der Server mit &#039;&#039;&#039;413 Payload Too Large&#039;&#039;&#039;. Für&lt;br /&gt;
Uploads gilt nicht das JSON-Body-Limit (10&amp;amp;nbsp;MB), sondern dieses Endpunkt-Limit.&lt;br /&gt;
Der Originalname (beim resumable Upload über den Header &amp;lt;code&amp;gt;Upload-Metadata&amp;lt;/code&amp;gt;) ist &#039;&#039;&#039;Pflicht&#039;&#039;&#039;; fehlt er, wird der Upload mit &#039;&#039;&#039;400&#039;&#039;&#039; abgelehnt, ohne dass eine Datei angelegt wird. Das Skript liest Pfad, Name, Content-Type und die SHA-256-Prüfsumme der gespeicherten Datei über &amp;lt;code&amp;gt;oReader.UploadPath()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;UploadName()&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;UploadType()&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;UploadSha()&amp;lt;/code&amp;gt;; bei &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; die übrigen Formularfelder über &amp;lt;code&amp;gt;oReader.Form(&#039;&amp;amp;lt;name&amp;amp;gt;&#039;)&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Das Übertragungsprotokoll&#039;&#039;&#039; - Header, Statuscodes, Resume-Verhalten - steht auf einer eigenen Seite: [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]].&lt;br /&gt;
&lt;br /&gt;
Unterstützt werden zwei Übertragungsarten:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Einfacher Upload&#039;&#039;&#039; per &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; (eine Datei pro Request).&lt;br /&gt;
* &#039;&#039;&#039;Resumable/Chunked Upload&#039;&#039;&#039; per &amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt; (grosse Dateien, fortsetzbar nach Abbruch).&lt;br /&gt;
&lt;br /&gt;
Der Server legt die hochgeladene Datei in einem temporären Verzeichnis ab und&lt;br /&gt;
übergibt dem Endpunkt-Skript den Pfad. Was mit der Datei geschieht (Ablage,&lt;br /&gt;
DMS-Verknüpfung, Weiterverarbeitung), entscheidet allein das Skript. Details und&lt;br /&gt;
Beispiele: [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
== Datei-Downloads ==&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt kann eine &#039;&#039;&#039;Datei&#039;&#039;&#039; statt eines JSON-Körpers zurückgeben -&lt;br /&gt;
PDF, CSV, eine APK. Dafür ist &#039;&#039;&#039;keine Freischaltung&#039;&#039;&#039; nötig: ein Download&lt;br /&gt;
entsteht dadurch, dass das Skript ihn mit &amp;lt;code&amp;gt;oWriter.SendFile(...)&amp;lt;/code&amp;gt;&lt;br /&gt;
bzw. &amp;lt;code&amp;gt;oWriter.SendTempFile(...)&amp;lt;/code&amp;gt; erzeugt, nicht dadurch, dass ein&lt;br /&gt;
Client ihn anfragt.&lt;br /&gt;
&lt;br /&gt;
Content-Type, Dateiname, Bereichsanfragen (&amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt;, &#039;&#039;&#039;206&#039;&#039;&#039;),&lt;br /&gt;
ETag und das Aufräumen temporärer Dateien übernimmt der Server. Einzelheiten:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]].&lt;br /&gt;
&lt;br /&gt;
== HTTP-Methode ==&lt;br /&gt;
&lt;br /&gt;
Welche Funktion aufgerufen wird, ergibt sich aus dem HTTP-Verb: Der Server ruft&lt;br /&gt;
die gleichnamige Skript-Methode auf (&amp;lt;code&amp;gt;Get&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Post&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Put&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Delete&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Patch&amp;lt;/code&amp;gt;).&lt;br /&gt;
Ein Template entspricht damit &#039;&#039;&#039;einem&#039;&#039;&#039; Endpunkt-Skript; unterschiedliche&lt;br /&gt;
Pfad-Formen (z.&amp;amp;nbsp;B. &amp;lt;code&amp;gt;/orders&amp;lt;/code&amp;gt; vs. &amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt;) sind&lt;br /&gt;
&#039;&#039;&#039;eigene Endpunkt-Einträge&#039;&#039;&#039; mit eigenem Skript und eigener Berechtigung.&lt;br /&gt;
&lt;br /&gt;
== URL-Struktur ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
http://[Host][:Port][Pfad-Template]&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Beispiele:&lt;br /&gt;
* &amp;lt;code&amp;gt;https://api.meinserver.de/orders&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;https://api.meinserver.de/orders/4711&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;https://api.meinserver.de/orders/4711/modules/A1&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Versionierung ==&lt;br /&gt;
&lt;br /&gt;
Da es kein eigenes Versions-Feld mehr gibt, wird die Version als statisches&lt;br /&gt;
Segment ins Template aufgenommen, z.&amp;amp;nbsp;B. &amp;lt;code&amp;gt;/orders/v1&amp;lt;/code&amp;gt; oder&lt;br /&gt;
&amp;lt;code&amp;gt;/v1/orders/{uid}&amp;lt;/code&amp;gt;. Änderung ohne Breaking Change:&lt;br /&gt;
&lt;br /&gt;
# Bestehenden Endpunkt unverändert lassen.&lt;br /&gt;
# Neuen Endpunkt mit gleichem Ressourcennamen, aber neuem Versions-Segment im Template anlegen.&lt;br /&gt;
# Berechtigungen für neue Konsumenten setzen.&lt;br /&gt;
# Alten Endpunkt deaktivieren, wenn die Migration abgeschlossen ist.&lt;br /&gt;
&lt;br /&gt;
== Zugriffskontrolle ==&lt;br /&gt;
&lt;br /&gt;
Endpunkte sind Server-Profilen zugeordnet; ein Zugang benötigt eine explizite&lt;br /&gt;
Berechtigung (Tabelle &amp;lt;code&amp;gt;RESTSRV_ACCESS&amp;lt;/code&amp;gt;), um einen Endpunkt nutzen zu&lt;br /&gt;
dürfen.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Ein Endpunkt besteht aus zwei Zeilen, und beide werden von Hand&lt;br /&gt;
gepflegt:&#039;&#039;&#039; der Eintrag in &amp;lt;code&amp;gt;RESTSRV_ENDPOINTS&amp;lt;/code&amp;gt; und die Berechtigung&lt;br /&gt;
in &amp;lt;code&amp;gt;RESTSRV_ACCESS&amp;lt;/code&amp;gt;. Fehlt die zweite, antwortet der Endpunkt&lt;br /&gt;
&#039;&#039;&#039;403&#039;&#039;&#039; - und sieht dabei fertig aus: Pfad, Skript und Server-Profil stehen&lt;br /&gt;
korrekt da, der Aufruf kommt trotzdem nicht durch. Das ist die häufigste Ursache&lt;br /&gt;
für ein „der Endpunkt ist doch angelegt&amp;quot; und kostet jedes Mal eine&lt;br /&gt;
Fehlersuche, weil der Statuscode nach einem Rechteproblem des Konsumenten&lt;br /&gt;
aussieht und nicht nach einer fehlenden Konfigurationszeile.&lt;br /&gt;
&lt;br /&gt;
Beim Anlegen also &#039;&#039;&#039;immer beide Zeilen&#039;&#039;&#039;, und beim Prüfen eines&lt;br /&gt;
&amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; zuerst nachsehen, ob die zweite existiert.}} Weil jede Pfad-Form ein eigener Endpunkt-Eintrag ist, lässt sich der&lt;br /&gt;
Zugriff &#039;&#039;&#039;pro Pfad-Form&#039;&#039;&#039; granular vergeben (z.&amp;amp;nbsp;B. Lesen von&lt;br /&gt;
&amp;lt;code&amp;gt;/orders/{uid}&amp;lt;/code&amp;gt; erlauben, aber das ändernde&lt;br /&gt;
&amp;lt;code&amp;gt;PUT /orders/{uid}/modules/{code}&amp;lt;/code&amp;gt; nicht).&lt;br /&gt;
&lt;br /&gt;
== Caching ==&lt;br /&gt;
&lt;br /&gt;
Die Endpunkt-Definitionen werden pro Server-Profil zwischengespeichert&lt;br /&gt;
(TTL, Standard 60&amp;amp;nbsp;s). Neue oder geänderte Endpunkte/Templates werden daher&lt;br /&gt;
erst nach Ablauf des Caches (bzw. nach einer Cache-Invalidierung) wirksam – nicht&lt;br /&gt;
zwingend sofort.&lt;br /&gt;
&lt;br /&gt;
== HTTP-Fehlercodes ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Code !! Ursache&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;404&#039;&#039;&#039; || Kein Template passt zum Pfad, Endpunkt inaktiv oder falsches Server-Profil&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;403&#039;&#039;&#039; || Zugang fehlt in der Berechtigungsliste&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;401&#039;&#039;&#039; || API-Key ungültig/fehlend, JWT abgelaufen oder Sitzung gesperrt (&#039;&#039;AUTH_EXPIRED&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;500&#039;&#039;&#039; || Skript- oder Syntax-Fehler (Details in &amp;lt;code&amp;gt;RESTSRV_PROTO&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;405&#039;&#039;&#039; || Das Endpunkt-Skript hat für die angefragte HTTP-Methode keine Funktion; der Header &amp;lt;code&amp;gt;Allow&amp;lt;/code&amp;gt; nennt die vorhandenen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;206&#039;&#039;&#039; || Teilantwort eines Datei-Downloads auf eine &amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt;-Anfrage; die Antwort enthält &amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;416&#039;&#039;&#039; || Der angefragte &amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt; liegt hinter dem Dateiende; die Antwort nennt in &amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt; die wirkliche Grösse&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;415&#039;&#039;&#039; || Datei-Upload an einen Endpunkt, der dafür nicht freigeschaltet ist (&amp;lt;code&amp;gt;re_upload = 0&amp;lt;/code&amp;gt;); oder ein &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt;, das der Server nicht entpacken kann - unterstützt wird allein &amp;lt;code&amp;gt;gzip&amp;lt;/code&amp;gt; (siehe [[OBS/Kostenpflichtige Module/RESTServer|REST-Server]], Abschnitt Kompression)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;413&#039;&#039;&#039; || Hochgeladene Datei überschreitet die zulässige Maximalgrösse (&amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;); oder der Anfragekörper überschreitet 10 MB bzw. - gepackt gesendet - das 400fache der gesendeten Grösse&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;429&#039;&#039;&#039; || Rate-Limit überschritten; die Antwort enthält den Header &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;503&#039;&#039;&#039; || Vier Ursachen, unterscheidbar am &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt;: &amp;lt;code&amp;gt;IDEMPOTENCY_IN_PROGRESS&amp;lt;/code&amp;gt; (eine Anfrage mit demselben &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; wird gerade verarbeitet), &amp;lt;code&amp;gt;SERVICE_UNAVAILABLE&amp;lt;/code&amp;gt; bei erreichter Andrangsgrenze des Server-Profils (&amp;lt;code&amp;gt;rsv_max_parallel&amp;lt;/code&amp;gt;), bei einer Anmeldung, deren Sitzungszeile nicht geschrieben werden konnte, sowie bei einem fehlgeschlagenen &amp;lt;code&amp;gt;JwtRevoke&amp;lt;/code&amp;gt;. Alle vier tragen &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;409&#039;&#039;&#039; || Ein früherer Aufruf mit demselben &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; ist ohne Ergebnis geblieben (&amp;lt;code&amp;gt;IDEMPOTENCY_UNRESOLVED&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;422&#039;&#039;&#039; || Derselbe &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; wurde mit &#039;&#039;&#039;anderem&#039;&#039;&#039; Inhalt gesendet (&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_REUSED&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;400&#039;&#039;&#039; || Ein schreibender Aufruf (POST/PUT/PATCH/DELETE) &#039;&#039;&#039;ohne&#039;&#039;&#039; &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; (&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt;); ein resumable Upload ohne Dateinamen; oder eine Anfrage, die unverschlüsselt an einen TLS-Port geschickt wurde (&amp;lt;code&amp;gt;http&amp;lt;/code&amp;gt; statt &amp;lt;code&amp;gt;https&amp;lt;/code&amp;gt;) - letztere wird vor Authentifizierung und Endpunkt-Skript abgewiesen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;308&#039;&#039;&#039; || Zwischenantwort beim resumable Upload: das Teilstück wurde angehängt, der Upload ist noch nicht vollständig. Die Antwort trägt &amp;lt;code&amp;gt;Upload-Id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Upload-Offset&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;Range&amp;lt;/code&amp;gt;; das Skript läuft dabei &#039;&#039;&#039;nicht&#039;&#039;&#039;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Jede Fehlerantwort besteht aus einem &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt;-Objekt mit&lt;br /&gt;
maschinenlesbarem Code - Aufbau siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer|Übersicht]].&lt;br /&gt;
&lt;br /&gt;
Daneben kann ein Endpunkt-Skript den Statuscode selbst setzen (z.B. &amp;lt;code&amp;gt;201&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;204&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;409&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;422&amp;lt;/code&amp;gt;) sowie Response-Header wie &amp;lt;code&amp;gt;ETag&amp;lt;/code&amp;gt; oder &amp;lt;code&amp;gt;Location&amp;lt;/code&amp;gt; - siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
== Idempotenz ==&lt;br /&gt;
&lt;br /&gt;
Bei &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;PUT&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;PATCH&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;DELETE&amp;lt;/code&amp;gt;&lt;br /&gt;
sorgt der Server selbst dafür, dass eine wiederholte Sendung &#039;&#039;&#039;keine&lt;br /&gt;
Zweitwirkung&#039;&#039;&#039; hat. Das gilt für &#039;&#039;&#039;jeden&#039;&#039;&#039; Endpunkt - es muss weder am Endpunkt&lt;br /&gt;
etwas eingestellt noch im Skript etwas programmiert werden.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Der Header &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; ist bei diesen vier Verben&lt;br /&gt;
Pflicht.&#039;&#039;&#039; Fehlt er, antwortet der Server mit &#039;&#039;&#039;400&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt; und das Endpunkt-Skript läuft &#039;&#039;&#039;nicht&#039;&#039;&#039; an.&lt;br /&gt;
&lt;br /&gt;
Bis v1.21 lief eine Anfrage ohne den Header ungeschützt durch; der Schutz gegen&lt;br /&gt;
die Doppelbuchung lag damit beim Client. Ausgenommen ist allein der&lt;br /&gt;
JWT-Endpunkt (Anmelden, Erneuern, Abmelden) - er greift vor der Adressauflösung&lt;br /&gt;
und kennt die Mechanik nicht.}}&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Situation !! Antwort des Servers&lt;br /&gt;
|-&lt;br /&gt;
| Kein &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; gesendet || &#039;&#039;&#039;400&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt;. Das Skript läuft nicht&lt;br /&gt;
|-&lt;br /&gt;
| Erster Aufruf || Endpunkt läuft normal, das Ergebnis wird zum Schlüssel gespeichert&lt;br /&gt;
|-&lt;br /&gt;
| Wiederholung, gleicher Inhalt || &#039;&#039;&#039;Gespeicherte Antwort&#039;&#039;&#039; (gleicher Statuscode, gleicher Body, gleiche Header) plus Header &amp;lt;code&amp;gt;Idempotent-Replay: true&amp;lt;/code&amp;gt;. Das Skript läuft &#039;&#039;&#039;nicht&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| Wiederholung, anderer Inhalt || &#039;&#039;&#039;422&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_KEY_REUSED&amp;lt;/code&amp;gt; - derselbe Schlüssel für einen anderen Inhalt ist ein Client-Fehler&lt;br /&gt;
|-&lt;br /&gt;
| Erster Aufruf läuft noch || &#039;&#039;&#039;503&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_IN_PROGRESS&amp;lt;/code&amp;gt; mit &amp;lt;code&amp;gt;Retry-After&amp;lt;/code&amp;gt;. Beim nächsten Versuch liegt die gespeicherte Antwort vor&lt;br /&gt;
|-&lt;br /&gt;
| Früherer Aufruf ohne Ergebnis || &#039;&#039;&#039;409&#039;&#039;&#039; &amp;lt;code&amp;gt;IDEMPOTENCY_UNRESOLVED&amp;lt;/code&amp;gt;. Nur noch nach einem &#039;&#039;&#039;harten Abbruch&#039;&#039;&#039; des Dienstes zwischen Verarbeitung und Festschreiben; ob die Buchung stattgefunden hat, ist unbekannt. Der Aufräumlauf gibt solche Einträge nach 24 Stunden frei und protokolliert sie&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wichtig für die Auslegung eines Endpunkts:&lt;br /&gt;
&lt;br /&gt;
* Antwortet das Skript mit &#039;&#039;&#039;2xx&#039;&#039;&#039;, wird die Antwort gespeichert und bei einer Wiederholung erneut ausgeliefert.&lt;br /&gt;
* &#039;&#039;&#039;Ausnahme Dateiantwort:&#039;&#039;&#039; Liefert das Skript mit &amp;lt;code&amp;gt;SendFile&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;SendTempFile&amp;lt;/code&amp;gt; eine &#039;&#039;&#039;Datei&#039;&#039;&#039; aus, wird &#039;&#039;&#039;nichts&#039;&#039;&#039; festgeschrieben - eine Dateiantwort liesse sich nicht wiedergeben, gespeichert würde eine leere 200er-Antwort. Der Schlüssel wird stattdessen wieder freigegeben, ein erneuter Abruf derselben Datei ist unschädlich.&lt;br /&gt;
* Antwortet das Skript mit &#039;&#039;&#039;4xx&#039;&#039;&#039; (fachliche Ablehnung), wird der Schlüssel wieder &#039;&#039;&#039;freigegeben&#039;&#039;&#039; - der Konsument darf ihn nach Korrektur erneut verwenden. Sonst würde ein einziger Validierungsfehler den Schlüssel dauerhaft blockieren.&lt;br /&gt;
* Endet die Verarbeitung mit &#039;&#039;&#039;5xx&#039;&#039;&#039; oder einem Skript-Fehler, wird der Schlüssel &#039;&#039;&#039;freigegeben&#039;&#039;&#039; und der Fall protokolliert. Der Konsument darf denselben Schlüssel erneut senden.&lt;br /&gt;
* Das ist eine bewusste Abwägung (geändert 2026-08-25): Bleibt der Schlüssel nach einem Serverfehler belegt, ist der Vorgang &#039;&#039;&#039;unwiederholbar&#039;&#039;&#039; - der Client bekommt dauerhaft 503 und müsste die erfasste Arbeit verwerfen. Garantierter Datenverlust wiegt schwerer als ein möglicher Doppelsatz.&lt;br /&gt;
* &#039;&#039;&#039;Folge für die Auslegung:&#039;&#039;&#039; ein schreibender Endpunkt sollte selbst wiederholbar sein, etwa über ein fachliches Merkmal des Clients. Wo reines Anhängen stattfindet (Positionen, Dateien), bleibt sonst ein Restrisiko.&lt;br /&gt;
&lt;br /&gt;
Der Schlüssel gilt je &#039;&#039;&#039;Zugang, Methode und Pfad&#039;&#039;&#039;. Derselbe&lt;br /&gt;
&amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; an einem anderen Endpunkt ist damit ein eigener&lt;br /&gt;
Vorgang - der Konsument muss ihn nicht global eindeutig vergeben, aber pro Vorgang&lt;br /&gt;
&#039;&#039;&#039;stabil wiederverwenden&#039;&#039;&#039; (also nicht bei jedem Wiederholversuch neu erzeugen).&lt;br /&gt;
&lt;br /&gt;
Die Einträge stehen in &amp;lt;code&amp;gt;RESTSRV_IDEMPOTENCY&amp;lt;/code&amp;gt;. Abgeschlossene Einträge&lt;br /&gt;
werden nach &#039;&#039;&#039;30 Tagen&#039;&#039;&#039; gelöscht. Einträge &#039;&#039;&#039;ohne Ergebnis&#039;&#039;&#039; - sie entstehen&lt;br /&gt;
nur bei einem harten Abbruch des Dienstes - werden ab &#039;&#039;&#039;24 Stunden&#039;&#039;&#039; im Protokoll&lt;br /&gt;
gemeldet und &#039;&#039;&#039;anschliessend ebenfalls gelöscht&#039;&#039;&#039;. Das Stehenlassen wäre keine&lt;br /&gt;
Vorsicht, sondern eine Sperre: derselbe Vorgang mit demselben Schlüssel wäre sonst&lt;br /&gt;
nie wieder durchführbar.&lt;br /&gt;
&lt;br /&gt;
== WebHooks ==&lt;br /&gt;
&lt;br /&gt;
WebHooks sind &#039;&#039;&#039;Einmal-Endpunkte&#039;&#039;&#039; für asynchrone Callbacks. Beim ersten&lt;br /&gt;
erfolgreichen Aufruf wird die Antwort in der Tabelle &amp;lt;code&amp;gt;REMOTE_HOOK_URL&amp;lt;/code&amp;gt;&lt;br /&gt;
gespeichert und der Endpunkt anschließend automatisch gelöscht.&lt;br /&gt;
&lt;br /&gt;
Typischer Einsatz: OBS stösst einen Vorgang bei einem Fremdsystem an (Bezahldienst,&lt;br /&gt;
Versanddienstleister) und gibt diesem eine Rückruf-Adresse mit, die nur &#039;&#039;&#039;ein&lt;br /&gt;
einziges Mal&#039;&#039;&#039; gültig ist. Damit kann die Adresse nicht später erneut - oder von&lt;br /&gt;
jemand anderem - benutzt werden.&lt;br /&gt;
&lt;br /&gt;
Ablauf:&lt;br /&gt;
&lt;br /&gt;
# OBS legt einen Eintrag in &amp;lt;code&amp;gt;REMOTE_HOOK_URL&amp;lt;/code&amp;gt; an und dazu einen Endpunkt, dessen Feld &#039;&#039;&#039;WebHook&#039;&#039;&#039; (&amp;lt;code&amp;gt;re_webhook&amp;lt;/code&amp;gt;) auf diesen Eintrag zeigt. Ein Skript wird für diesen Endpunkt nicht gepflegt.&lt;br /&gt;
# Die Adresse dieses Endpunkts wird dem Fremdsystem als Callback-URL übergeben.&lt;br /&gt;
# Das Fremdsystem ruft die Adresse auf. Der Server legt den übergebenen Inhalt in &amp;lt;code&amp;gt;REMOTE_HOOK_URL.hu_response&amp;lt;/code&amp;gt; ab, antwortet mit &amp;lt;code&amp;gt;{&amp;quot;status&amp;quot;: &amp;quot;ok&amp;quot;}&amp;lt;/code&amp;gt; und &#039;&#039;&#039;löscht den Endpunkt&#039;&#039;&#039;.&lt;br /&gt;
# Der weiterverarbeitende OBS-Prozess findet die Rückmeldung in &amp;lt;code&amp;gt;hu_response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Ein zweiter Aufruf derselben Adresse läuft ins Leere: Der Endpunkt&lt;br /&gt;
existiert nicht mehr, die Antwort ist 404. Das ist gewollt - ein WebHook ist keine&lt;br /&gt;
dauerhafte Schnittstelle. Für eine dauerhaft erreichbare Rückmelde-Adresse einen&lt;br /&gt;
normalen Endpunkt mit Skript anlegen.}}&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
	<entry>
		<id>https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer&amp;diff=65541</id>
		<title>OBS/Kostenpflichtige Module/RESTServer</title>
		<link rel="alternate" type="text/html" href="https://wiki.bergau.de/index.php?title=OBS/Kostenpflichtige_Module/RESTServer&amp;diff=65541"/>
		<updated>2026-09-25T09:28:32Z</updated>

		<summary type="html">&lt;p&gt;MINERVA-Rademacker: Idempotency-Key als Pflicht, Abschnitt Keep-alive, HSTS, vollstaendige Konsolenbefehle; Ablaufliste und Protokollverhalten berichtigt [Volltext ersetzt]&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Kostenpflichtige Module}}&lt;br /&gt;
=REST-Server=&lt;br /&gt;
{{Hinweis|Es handelt sich um ein kostenpflichtiges Modul. Die Schnittstelle muss über den OBS-Support aktiviert werden.}}&lt;br /&gt;
&lt;br /&gt;
==Was leistet der REST-Server?==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server ist die &#039;&#039;&#039;universelle Schnittstelle&#039;&#039;&#039; Ihrer OBS-Installation nach außen. Er macht aus Ihrem OBS einen Dienst, mit dem andere Programme - Webseiten, Apps, Geräte, Kundensysteme, Cloud-Dienste - direkt sprechen können. Statt Daten manuell zu exportieren, zu mailen oder über Umwege bereitzustellen, holen sich angebundene Systeme genau die Information, die sie brauchen, in dem Moment, in dem sie sie brauchen - oder liefern neue Daten direkt in OBS ab.&lt;br /&gt;
&lt;br /&gt;
Für Anwender, die nicht selbst programmieren, bedeutet das: &#039;&#039;&#039;Was bisher nur manuell, per Datei-Import oder über Spezialschnittstellen ging, lässt sich jetzt automatisieren.&#039;&#039;&#039; Eine Webseite zeigt live aktuelle Lagerbestände. Mitarbeiter erfassen Zeiten über das Handy, ohne dass jemand Excel-Listen einpflegen muss. Ein Kunde stößt per Bestelltaste in seinem System einen Vorgang in Ihrem OBS an. Ein Lieferant meldet Wareneingänge automatisch zurück. Jeder dieser Anwendungsfälle wird einmal eingerichtet und läuft danach automatisch.&lt;br /&gt;
&lt;br /&gt;
Technisch gesehen ist der REST-Server ein in OBS integrierter, voll konfigurierbarer &#039;&#039;&#039;HTTP/HTTPS-Dienst&#039;&#039;&#039;, dessen Endpunkte über in OBS gepflegte Pascal-Skripte realisiert werden. Jeder Endpunkt bekommt eine eigene Adresse, eine eigene Logik und eine eigene Berechtigungsstruktur. Rückgaben erfolgen ausschliesslich im JSON-Format. Damit lassen sich beliebige Lese- und Schreibvorgänge auf der OBS-Datenbank realisieren, ohne dass externe Systeme direkten Datenbank-Zugriff erhalten - das ganze OBS-Regelwerk (Rechte, Validierung, Geschäftslogik) bleibt aktiv.&lt;br /&gt;
&lt;br /&gt;
Im Ergebnis ist der REST-Server kein einzelnes Feature, sondern ein &#039;&#039;&#039;Werkzeugkasten&#039;&#039;&#039;: Was an Daten oder Funktionen in OBS verfügbar ist, kann über den REST-Server auch nach außen angeboten - oder von außen entgegengenommen - werden. Die Bandbreite reicht vom einfachen Lese-Endpunkt für ein Webseiten-Widget bis hin zu kompletten B2B-Integrationen mit Mandanten- und Rollen-Trennung.&lt;br /&gt;
&lt;br /&gt;
Aufruf des Moduls in OBS:&lt;br /&gt;
&#039;&#039;&#039;Stammdaten&#039;&#039;&#039; -&amp;gt; &#039;&#039;&#039;Z Weitere Stammdaten&#039;&#039;&#039; -&amp;gt; &#039;&#039;&#039;REST-Server&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Anwendungsbereiche==&lt;br /&gt;
&lt;br /&gt;
Die folgenden Einsatzfälle sind typische Beispiele, an denen sich der praktische Nutzen ablesen lässt. Es ist keine abschliessende Liste - alles, was sich in OBS-Skripten abbilden lässt, kann auch über einen Endpunkt bereitgestellt werden.&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Dies sind Beispiele. Für die konkrete Entwicklung können, je nach Komplexität, weitere Kosten anfallen.}}&lt;br /&gt;
&lt;br /&gt;
===Webseiten und Kundenportale===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Live-Daten auf der eigenen Webseite:&#039;&#039;&#039; Lagerbestände, Verfügbarkeiten, Preise, Auftragsstatus aus OBS direkt anzeigen, ohne tägliche Datei-Exports.&lt;br /&gt;
* &#039;&#039;&#039;Kundenportale:&#039;&#039;&#039; Kunden sehen ihre offenen Posten, Auftragshistorie, Lieferscheine. Die Seite holt sich die Daten zur Anzeigezeit direkt aus OBS, jeder Kunde sieht nur seine eigenen Daten.&lt;br /&gt;
* &#039;&#039;&#039;Trackingseiten:&#039;&#039;&#039; Status einer Bestellung, eines Auftrags oder einer Reparatur für Endkunden, abrufbar über eine Sendungsnummer.&lt;br /&gt;
&lt;br /&gt;
===Mobile Anwendungen und Außendienst===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Mobile Zeiterfassung:&#039;&#039;&#039; Mitarbeiter buchen Anfang, Pause und Ende über Smartphone oder Tablet, die Daten landen direkt in OBS - ohne Zettelwirtschaft.&lt;br /&gt;
* &#039;&#039;&#039;QR-/Barcode-Scanner und Lagergeräte:&#039;&#039;&#039; Wareneingang, Inventur, Kommissionierung direkt in OBS abbilden, ohne zwischengeschaltete Software.&lt;br /&gt;
* &#039;&#039;&#039;Außendienst-Apps:&#039;&#039;&#039; Techniker sehen ihre Aufträge, dokumentieren Einsätze, erfassen Material - synchronisiert mit OBS.&lt;br /&gt;
&lt;br /&gt;
===Anbindung von Drittsystemen===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Webshops:&#039;&#039;&#039; Bestellungen werden direkt vom Shop in OBS gemeldet, Lagerbestände und Preise vom Shop bei OBS abgefragt.&lt;br /&gt;
* &#039;&#039;&#039;Buchhaltungs- und ERP-Systeme:&#039;&#039;&#039; Bidirektionaler Austausch von Belegen, Stammdaten und Buchungen.&lt;br /&gt;
* &#039;&#039;&#039;CRM- und Marketing-Tools:&#039;&#039;&#039; Personendaten, Aktivitäten oder Mailing-Empfänger werden synchron gehalten.&lt;br /&gt;
* &#039;&#039;&#039;Logistikdienstleister:&#039;&#039;&#039; Versandaufträge werden übergeben, Tracking-Informationen zurückgeliefert.&lt;br /&gt;
&lt;br /&gt;
===Automatisierte Rückmeldungen (WebHooks)===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Zahlungs-Callbacks:&#039;&#039;&#039; Bezahldienste (z.B. PayPal, Klarna, Stripe) melden den Zahlungseingang an einen WebHook-Endpunkt - OBS bucht automatisch.&lt;br /&gt;
* &#039;&#039;&#039;Versanddienste:&#039;&#039;&#039; Statusmeldungen (verschickt, zugestellt, retour) werden direkt im passenden Vorgang dokumentiert.&lt;br /&gt;
* &#039;&#039;&#039;Cloud-Workflows:&#039;&#039;&#039; Externe Automatisierungs-Plattformen (Make, Zapier, n8n, ...) stoßen OBS-Vorgänge an oder werden von OBS aus angesprochen.&lt;br /&gt;
&lt;br /&gt;
===Geschäftspartner-Kommunikation (B2B)===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Lieferanten-Schnittstelle:&#039;&#039;&#039; Bestellungen werden automatisch übergeben, Auftragsbestätigungen und Lieferavise zurückgespielt.&lt;br /&gt;
* &#039;&#039;&#039;Kunden-Schnittstelle:&#039;&#039;&#039; Grosskunden bestellen direkt aus ihrem ERP heraus, Stammdaten und Konditionen werden zentral gepflegt.&lt;br /&gt;
* &#039;&#039;&#039;Sichere Maschine-zu-Maschine-Kommunikation:&#039;&#039;&#039; Mit Mutual-TLS (mTLS) wird sichergestellt, dass nur die freigeschalteten Partnersysteme - und niemand sonst - mit OBS sprechen können.&lt;br /&gt;
&lt;br /&gt;
===Interne Microservices und Automatisierungen===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Eigene kleine Hilfs-Tools:&#039;&#039;&#039; Zeitschaltautomatik, Reporting-Generator, Sammelaktionen, die aus mehreren Quellen Daten in OBS verarbeiten.&lt;br /&gt;
* &#039;&#039;&#039;Verbindung mehrerer Standorte:&#039;&#039;&#039; Verteilte Systeme tauschen Daten über definierte Endpunkte aus, ohne offene Datenbankverbindungen.&lt;br /&gt;
* &#039;&#039;&#039;Datenbereitstellung für Dashboards und Auswertungen:&#039;&#039;&#039; BI-Tools (Power BI, Grafana, ...) lesen aufbereitete Kennzahlen direkt aus OBS.&lt;br /&gt;
&lt;br /&gt;
===Was bedeutet das wirtschaftlich?===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Manuelle Arbeit fällt weg.&#039;&#039;&#039; Daten werden nicht mehr aus OBS exportiert, per Mail geschickt und woanders eingelesen - sie fliessen direkt.&lt;br /&gt;
* &#039;&#039;&#039;Fehler werden weniger.&#039;&#039;&#039; Jede manuelle Stelle ist eine mögliche Fehlerquelle; jede Automatisierung eine Stelle, an der nichts mehr schiefgehen kann.&lt;br /&gt;
* &#039;&#039;&#039;Neue Geschäftsmodelle werden möglich.&#039;&#039;&#039; Kunden- und Lieferantenportale, Self-Service-Funktionen, App-Anbindungen lassen sich aufbauen, ohne ein zweites System pflegen zu müssen.&lt;br /&gt;
* &#039;&#039;&#039;Bestehende Software bleibt verbunden.&#039;&#039;&#039; Statt eine vorhandene Anwendung ablösen zu müssen, kann sie über den REST-Server an OBS angedockt werden - in beiden Richtungen.&lt;br /&gt;
* &#039;&#039;&#039;Skaliert mit:&#039;&#039;&#039; Was klein anfängt (ein einzelner Endpunkt für eine Webseite) kann zu einer kompletten Integrations-Plattform ausgebaut werden, ohne dass die Grundlage gewechselt werden muss.&lt;br /&gt;
&lt;br /&gt;
==Architektur im Überblick==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server besteht aus mehreren aufeinander aufbauenden Bausteinen, die alle in OBS gepflegt werden:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Baustein     !! Tabelle              !! Zweck&lt;br /&gt;
|-&lt;br /&gt;
| Server       || RESTSRV_SERVER       || TLS-Profil + eigene HTTP-Server-Instanz, kann mehrfach existieren&lt;br /&gt;
|-&lt;br /&gt;
| Bindung      || RESTSRV_BINDINGS     || IP-Adresse + Port pro Server&lt;br /&gt;
|-&lt;br /&gt;
| Zugang       || RESTSRV_ACCOUNT      || API-Key, optionale Host- und CORS-Beschränkung, optionale JWT-Konfiguration&lt;br /&gt;
|-&lt;br /&gt;
| Endpunkt     || RESTSRV_ENDPOINTS    || Skript, das die Anfrage bearbeitet, einem Server fest zugeordnet&lt;br /&gt;
|-&lt;br /&gt;
| Berechtigung || RESTSRV_ACCESS       || Verbindet Zugang und Endpunkt&lt;br /&gt;
|-&lt;br /&gt;
| Idempotenz   || RESTSRV_IDEMPOTENCY  || Speicher für Idempotency-Keys und die dazu gelieferte Antwort&lt;br /&gt;
|-&lt;br /&gt;
| Sitzung      || RESTSRV_TOKEN        || Ausgestellte Token-Paare je Sitzung; Grundlage für Rotation und Widerruf&lt;br /&gt;
|-&lt;br /&gt;
| Protokoll    || RESTSRV_PROTO        || Laufende Ereignisse, Fehler, Audit-Einträge&lt;br /&gt;
|-&lt;br /&gt;
| Statistik    || RESTSRV_STATS        || Aufrufzähler, Antwortzeiten und HTTP-Status pro Endpunkt&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Eine Anfrage durchläuft folgende Stationen:&lt;br /&gt;
&lt;br /&gt;
# Rate-Limit-Prüfung (pro API-Key bzw. IP und global, Schwellen je Server-Profil)&lt;br /&gt;
# Andrangsgrenze des Server-Profils (&#039;&#039;Max. parallel&#039;&#039;) - bei Überschreitung &#039;&#039;&#039;503&#039;&#039;&#039; mit &#039;&#039;Retry-After&#039;&#039;&lt;br /&gt;
# Authentifizierung über den API-Key (Header &#039;&#039;apikey&#039;&#039;)&lt;br /&gt;
# CORS-Prüfung (sofern &#039;&#039;Origin&#039;&#039;-Header gesetzt)&lt;br /&gt;
# Optional: JWT-Prüfung - Signatur, Ablauf und Sitzung (sofern für den Zugang aktiviert)&lt;br /&gt;
# Routing: Auflösung des Pfads über die Pfad-Templates der Endpunkte&lt;br /&gt;
# Prüfung der Berechtigung (Zugang -&amp;gt; Endpunkt)&lt;br /&gt;
# Entpacken des Anfragekörpers, sofern er mit &#039;&#039;Content-Encoding: gzip&#039;&#039; gesendet wurde&lt;br /&gt;
# Idempotenz-Prüfung bei POST/PUT/PATCH/DELETE - der Header &#039;&#039;Idempotency-Key&#039;&#039; ist dort &#039;&#039;&#039;Pflicht&#039;&#039;&#039;&lt;br /&gt;
# Ausführung des Endpunkt-Skripts (oder WebHook-Antwort)&lt;br /&gt;
# Festschreiben des Ergebnisses im Idempotenz-Speicher (sofern in Schritt 9 reserviert)&lt;br /&gt;
# JSON-Antwort und Statistik-Eintrag&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Die Reihenfolge ist kein Zufall.&#039;&#039;&#039; Das Entpacken liegt &#039;&#039;&#039;hinter&#039;&#039;&#039;&lt;br /&gt;
der Authentifizierung, damit ein Unbefugter dem Server keine Rechenzeit für eine&lt;br /&gt;
Dekompressionsbombe abverlangen kann - und &#039;&#039;&#039;vor&#039;&#039;&#039; der Idempotenz-Reservierung,&lt;br /&gt;
damit eine abgewiesene Anfrage keinen &#039;&#039;Idempotency-Key&#039;&#039; verbrennt.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Der Normalfall erzeugt keine Protokollzeile.&#039;&#039;&#039; Bis 08/2026 schrieb jede&lt;br /&gt;
Anfrage eine Zeile nach RESTSRV_PROTO; sie ist entfallen, weil RESTSRV_STATS&lt;br /&gt;
zu derselben Anfrage IP, Zugang, Endpunkt, Methode, Pfad, Status, Laufzeit und&lt;br /&gt;
dieselbe Korrelations-ID trägt - und länger aufbewahrt wird. Das Protokoll trägt&lt;br /&gt;
seitdem nur noch Auffälligkeiten.}}&lt;br /&gt;
&lt;br /&gt;
==Adressierung==&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt wird über ein &#039;&#039;&#039;Pfad-Template&#039;&#039;&#039; angesprochen, das den Pfad nach dem Host beschreibt und Platzhalter enthalten kann:&lt;br /&gt;
&lt;br /&gt;
 http://[Hostadresse][:Port][Pfad-Template]&lt;br /&gt;
&lt;br /&gt;
Beispiele:&lt;br /&gt;
&lt;br /&gt;
 https://api.meinserver.de/kalender/v1&lt;br /&gt;
 https://api.meinserver.de/orders/{uid}&lt;br /&gt;
 https://api.meinserver.de/orders/{uid}/modules/{code}&lt;br /&gt;
&lt;br /&gt;
Statische Segmente müssen exakt passen; Platzhalter in geschweiften Klammern (z.B. {uid}) matchen einen beliebigen Wert und stehen im Skript als Pfad-Parameter zur Verfügung. Eine Versionskennung wird als statisches Segment ins Template aufgenommen (z.B. /kalender/v1), um eine Funktionalität zu verändern, ohne bestehende Konsumenten zu brechen. Details: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]].&lt;br /&gt;
&lt;br /&gt;
==Unterstützte HTTP-Methoden==&lt;br /&gt;
&lt;br /&gt;
Der Server akzeptiert die Methoden &#039;&#039;&#039;GET&#039;&#039;&#039;, &#039;&#039;&#039;POST&#039;&#039;&#039;, &#039;&#039;&#039;PUT&#039;&#039;&#039;, &#039;&#039;&#039;DELETE&#039;&#039;&#039; und &#039;&#039;&#039;PATCH&#039;&#039;&#039; sowie &#039;&#039;&#039;OPTIONS&#039;&#039;&#039; (CORS-Preflight, ohne Skript-Aufruf). Jede Methode wird im Endpunkt-Skript als gleichnamige Prozedur implementiert - &#039;&#039;&#039;mit einer Ausnahme&#039;&#039;&#039;: &#039;&#039;Delete&#039;&#039; ist in der Skriptsprache ein reservierter Name und lässt sich nicht als Prozedur deklarieren. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
Fehlt die zur Methode passende Prozedur, antwortet der Server mit &#039;&#039;&#039;405&#039;&#039;&#039; und&lt;br /&gt;
nennt im Header &#039;&#039;Allow&#039;&#039; die Verben, die dieses Skript tatsächlich anbietet.&lt;br /&gt;
&lt;br /&gt;
==Kompression==&lt;br /&gt;
&lt;br /&gt;
Der Server packt Antworten mit &#039;&#039;&#039;gzip&#039;&#039;&#039;, wenn der Client sie annimmt. Ausgehandelt wird über den Anfrage-Header &amp;lt;code&amp;gt;Accept-Encoding&amp;lt;/code&amp;gt;; die Antwort trägt dann &amp;lt;code&amp;gt;Content-Encoding: gzip&amp;lt;/code&amp;gt; und &amp;lt;code&amp;gt;Vary: Accept-Encoding&amp;lt;/code&amp;gt; - bei einer CORS-Anfrage steht dort &amp;lt;code&amp;gt;Vary: Origin, Accept-Encoding&amp;lt;/code&amp;gt;, also &#039;&#039;&#039;eine&#039;&#039;&#039; Zeile mit beiden Bedingungen. Browser, &amp;lt;code&amp;gt;curl --compressed&amp;lt;/code&amp;gt; und die gängigen HTTP-Bibliotheken entpacken selbstständig - für den Konsumenten ändert sich nichts ausser der übertragenen Datenmenge.&lt;br /&gt;
&lt;br /&gt;
Gepackt wird nur, wo es etwas bringt:&lt;br /&gt;
&lt;br /&gt;
* Nur JSON (auch &#039;&#039;problem+json&#039;&#039; und &#039;&#039;ndjson&#039;&#039;), XML, JavaScript, SVG und alles unter &#039;&#039;text/&#039;&#039; - darunter Text, CSV und HTML. Bereits komprimierte Typen - PDF, Bild, ZIP, Video - werden durch gzip &#039;&#039;&#039;grösser&#039;&#039;&#039;, nicht kleiner.&lt;br /&gt;
* Erst ab &#039;&#039;&#039;1024 Byte&#039;&#039;&#039;. Darunter kostet der gzip-Rahmen mehr, als der Inhalt einspart.&lt;br /&gt;
* &#039;&#039;&#039;Nicht&#039;&#039;&#039; bei Dateiantworten, nicht bei Teilauslieferungen (206) und nicht bei Antworten ohne Körper (204, 304).&lt;br /&gt;
* Nicht, wenn das Endpunkt-Skript den Header &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; selbst gesetzt hat - dann gehört die Antwort dem Skript.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;deflate&#039;&#039;&#039; wird in &#039;&#039;&#039;keiner&#039;&#039;&#039; Richtung unterstützt. Der Wert ist historisch uneindeutig - die einen senden RFC-1950-gerahmt, die anderen roh -, und wer ihn nennt, beherrscht ohnehin gzip. Eine Anfrage mit &amp;lt;code&amp;gt;Content-Encoding: deflate&amp;lt;/code&amp;gt; wird mit 415 abgewiesen.&lt;br /&gt;
&lt;br /&gt;
Abschalten lässt sich das Packen je Server-Profil über den Haken &#039;&#039;Keine Kompression&#039;&#039; (siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]). Der Haken betrifft ausschliesslich die Antwortrichtung.&lt;br /&gt;
&lt;br /&gt;
===Komprimierte Anfragen===&lt;br /&gt;
&lt;br /&gt;
In der Gegenrichtung nimmt der Server &#039;&#039;&#039;gzip&#039;&#039;&#039; entgegen: Trägt eine Anfrage den Header &amp;lt;code&amp;gt;Content-Encoding: gzip&amp;lt;/code&amp;gt;, wird der Körper entpackt, bevor ihn jemand liest. Das Endpunkt-Skript merkt davon nichts.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Anfrage                                              !! Antwort&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Content-Encoding&amp;lt;/code&amp;gt; fehlt oder &amp;lt;code&amp;gt;identity&amp;lt;/code&amp;gt; || normale Verarbeitung; ab 10 MB Körpergrösse &#039;&#039;&#039;413&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;gzip&amp;lt;/code&amp;gt;                                    || Körper wird entpackt&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;deflate&amp;lt;/code&amp;gt;, anderer Wert (z.B. &amp;lt;code&amp;gt;br&amp;lt;/code&amp;gt;) oder mehrere Werte || &#039;&#039;&#039;415&#039;&#039;&#039; &amp;lt;code&amp;gt;UNSUPPORTED_MEDIA_TYPE&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Gepackter Körper bei einem Datei-Upload              || &#039;&#039;&#039;415&#039;&#039;&#039; - siehe [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]]&lt;br /&gt;
|-&lt;br /&gt;
| Entpackter Körper über &#039;&#039;&#039;10 MB&#039;&#039;&#039;                    || &#039;&#039;&#039;413&#039;&#039;&#039; &amp;lt;code&amp;gt;PAYLOAD_TOO_LARGE&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Entpackter Körper über dem &#039;&#039;&#039;400fachen&#039;&#039;&#039; der gesendeten Grösse || &#039;&#039;&#039;413&#039;&#039;&#039; &amp;lt;code&amp;gt;PAYLOAD_TOO_LARGE&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Für einen gepackten Anfragekörper gelten &#039;&#039;&#039;zwei&#039;&#039;&#039; Grenzen. Beide zählen den &#039;&#039;&#039;entpackten&#039;&#039;&#039; Inhalt, und beide greifen bereits &#039;&#039;&#039;während&#039;&#039;&#039; des Entpackens - ein Körper, der sie reisst, belegt den Speicher gar nicht erst:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;10 MB&#039;&#039;&#039; absolut. Dieselbe Grenze, die auch für einen ungepackten Körper gilt.&lt;br /&gt;
* &#039;&#039;&#039;das 400fache&#039;&#039;&#039; der gesendeten Grösse, &#039;&#039;&#039;mindestens aber 64 KB&#039;&#039;&#039;. Damit wird eine &#039;&#039;Dekompressionsbombe&#039;&#039; - wenige Kilobyte, die sich auf Hunderte Megabyte aufblähen - schon nach wenigen Prozent abgebrochen. Der Boden von 64 KB sorgt dafür, dass die Verhältnisrechnung winzige Körper gar nicht erst trifft: ein 100-Byte-Körper darf sich auf 64 KB entpacken, nicht nur auf 40 KB.&lt;br /&gt;
&lt;br /&gt;
Die zweite Grenze trifft normale Nutzlasten nicht. Gemessen erreichen echte JSON-Körper Verhältnisse von 24:1 bis 40:1, und selbst eine Liste aus zehntausenden identischen Zeilen kommt nur auf rund 340:1 - mehr gibt das Verfahren für wiederholte Datensätze nicht her. Über 400:1 kommt nur, wer sehr lange Läufe desselben Zeichens sendet.&lt;br /&gt;
&lt;br /&gt;
Für Datei-Uploads gilt keine der beiden Grenzen - dort zählt allein die je Endpunkt eingestellte Maximalgrösse.}}&lt;br /&gt;
&lt;br /&gt;
==Persistente Verbindungen (Keep-alive)==&lt;br /&gt;
&lt;br /&gt;
Der Server hält eine Verbindung nach der Antwort &#039;&#039;&#039;offen&#039;&#039;&#039;, statt sie zu&lt;br /&gt;
schliessen. Ein Client, der mehrere Anfragen hintereinander stellt - eine App,&lt;br /&gt;
die eine Liste seitenweise abholt -, spart damit je Folgeanfrage den Aufbau von&lt;br /&gt;
TCP-Verbindung und TLS-Sitzung.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Grenze !! Wert !! Bedeutung&lt;br /&gt;
|-&lt;br /&gt;
| Leerlauf || &#039;&#039;&#039;10 Sekunden&#039;&#039;&#039; || Passiert auf der Verbindung so lange nichts, wird sie geschlossen&lt;br /&gt;
|-&lt;br /&gt;
| Anfragen je Verbindung || &#039;&#039;&#039;100&#039;&#039;&#039; || Danach schliesst der Server geordnet, der Client baut neu auf&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beides ist für den Konsumenten unkritisch: Eine geschlossene Verbindung ist kein&lt;br /&gt;
Fehler, jede HTTP-Bibliothek baut sie bei der nächsten Anfrage selbst wieder auf.&lt;br /&gt;
Wer viele Anfragen hintereinander stellt, gewinnt aber deutlich, wenn er seinen&lt;br /&gt;
Client die Verbindung wiederverwenden lässt (&#039;&#039;Connection: keep-alive&#039;&#039; ist der&lt;br /&gt;
Standard von HTTP/1.1, die meisten Bibliotheken tun es von allein).&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|&#039;&#039;&#039;Die Leerlaufgrenze gilt für jeden Lesevorgang der Verbindung&#039;&#039;&#039;,&lt;br /&gt;
nicht nur für das Warten auf die nächste Anfrage. Ein Client, der seinen&lt;br /&gt;
Anfragekörper nur tropfenweise sendet und dazwischen länger als 10 Sekunden&lt;br /&gt;
pausiert, verliert die Verbindung mitten im Senden.}}&lt;br /&gt;
&lt;br /&gt;
Die Werte sind bewusst knapp gewählt: Der Server fährt einen eigenen Thread je&lt;br /&gt;
Verbindung, eine leerlaufende Verbindung kostet also nicht bloss einen&lt;br /&gt;
Dateizeiger. Sie sind keine Lastgrenze - dafür gibt es &#039;&#039;Max. Verbindungen&#039;&#039; und&lt;br /&gt;
&#039;&#039;Max. parallel&#039;&#039; am Server-Profil (siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]]).&lt;br /&gt;
&lt;br /&gt;
==Datei-Uploads==&lt;br /&gt;
&lt;br /&gt;
Endpunkte, die dafür freigeschaltet sind (&amp;lt;code&amp;gt;re_upload&amp;lt;/code&amp;gt;), nehmen Dateien&lt;br /&gt;
per &amp;lt;code&amp;gt;multipart/form-data&amp;lt;/code&amp;gt; oder als fortsetzbaren &#039;&#039;&#039;Resumable-Upload&#039;&#039;&#039;&lt;br /&gt;
(&amp;lt;code&amp;gt;Content-Range&amp;lt;/code&amp;gt;) entgegen. Die maximale Grösse wird pro Endpunkt&lt;br /&gt;
festgelegt (&amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt;, Standard 25&amp;amp;nbsp;MB); Überschreitung&lt;br /&gt;
ergibt 413, ein Upload an einen nicht freigeschalteten Endpunkt 415. Der Server&lt;br /&gt;
legt die Datei temporär ab und übergibt dem Skript den Pfad; die weitere&lt;br /&gt;
Verarbeitung (z.B. DMS-Ablage) übernimmt das Skript. Details:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
==Doppelte Sendungen abfangen (Idempotenz)==&lt;br /&gt;
&lt;br /&gt;
Ein Client, der nach einem Timeout dieselbe Anfrage erneut schickt, darf keine&lt;br /&gt;
Zweitbuchung auslösen. Der Server erledigt das selbst: Jeder Aufruf mit&lt;br /&gt;
&#039;&#039;&#039;POST&#039;&#039;&#039;, &#039;&#039;&#039;PUT&#039;&#039;&#039;, &#039;&#039;&#039;PATCH&#039;&#039;&#039; oder &#039;&#039;&#039;DELETE&#039;&#039;&#039; muss den Header&lt;br /&gt;
&amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; tragen; der Server merkt sich Schlüssel und Ergebnis&lt;br /&gt;
in &#039;&#039;&#039;RESTSRV_IDEMPOTENCY&#039;&#039;&#039;. Eine Wiederholung mit demselben Schlüssel und&lt;br /&gt;
demselben Inhalt bekommt die &#039;&#039;&#039;gespeicherte Antwort&#039;&#039;&#039; zurück - das&lt;br /&gt;
Endpunkt-Skript läuft gar nicht erst an.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;&#039;Der Header ist Pflicht, nicht optional.&#039;&#039;&#039; Ein schreibender Aufruf&lt;br /&gt;
&#039;&#039;&#039;ohne&#039;&#039;&#039; &amp;lt;code&amp;gt;Idempotency-Key&amp;lt;/code&amp;gt; wird mit &#039;&#039;&#039;400&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;code&amp;gt;IDEMPOTENCY_KEY_MISSING&amp;lt;/code&amp;gt; abgewiesen, und das Endpunkt-Skript läuft&lt;br /&gt;
nicht an.&lt;br /&gt;
&lt;br /&gt;
Das war bis v1.21 anders: fehlte der Header, lief die Anfrage ungeschützt durch.&lt;br /&gt;
Der Schutz gegen eine Doppelbuchung stand damit in der Zusage des Clients und&lt;br /&gt;
nicht in einer Prüfung des Servers - ein Wiederholversuch nach Zeitüberschreitung&lt;br /&gt;
buchte ein zweites Mal. Konsumenten, die bisher ohne den Header schreiben, müssen&lt;br /&gt;
angepasst werden.}}&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Pflicht nur für schreibende Verben.&#039;&#039;&#039; &#039;&#039;GET&#039;&#039; braucht keinen Schlüssel und kennt die Mechanik nicht.&lt;br /&gt;
* &#039;&#039;&#039;Der JWT-Endpunkt ist ausgenommen.&#039;&#039;&#039; Anmelden, Erneuern und Abmelden laufen vor der Adressauflösung und brauchen keinen Schlüssel.&lt;br /&gt;
* &#039;&#039;&#039;Kein Schalter am Endpunkt.&#039;&#039;&#039; Die Mechanik greift für jeden Endpunkt automatisch.&lt;br /&gt;
* &#039;&#039;&#039;Das Skript muss nichts tun.&#039;&#039;&#039; Eine eigene Idempotenz-Logik im Skript ist nicht nötig.&lt;br /&gt;
* &#039;&#039;&#039;Pro Vorgang ein Schlüssel&#039;&#039;&#039;, über alle Wiederholversuche hinweg unverändert - nicht bei jedem Versuch neu erzeugt.&lt;br /&gt;
&lt;br /&gt;
Details und die Statuscodes: [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]], Abschnitt Idempotenz.&lt;br /&gt;
&lt;br /&gt;
==Sitzungen und Abmelden==&lt;br /&gt;
&lt;br /&gt;
Ein JWT trägt seine Gültigkeit in sich: der Server prüft Signatur und Ablauf und&lt;br /&gt;
braucht dafür keine Datenbank. Genau deshalb liesse sich ein ausgegebener Token&lt;br /&gt;
von sich aus auch nicht mehr zurückziehen - ein verlorenes Gerät wäre bis zum&lt;br /&gt;
Ablauf des Tokens weiter nutzbar.&lt;br /&gt;
&lt;br /&gt;
Der Server führt deshalb zu jedem ausgestellten Token-Paar eine Zeile in&lt;br /&gt;
&#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; und prüft jeden Zugriff dagegen. Daraus ergeben sich drei&lt;br /&gt;
Eigenschaften:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Abmelden wirkt sofort.&#039;&#039;&#039; Ein &#039;&#039;DELETE&#039;&#039; auf den JWT-Endpunkt sperrt die Sitzung; alle Token dieser Sitzung sind damit unmittelbar ungültig, auch die noch nicht abgelaufenen. Anmelden, Erneuern und Abmelden laufen also über einen einzigen Endpunkt, und für das Abmelden ist kein Skript nötig.&lt;br /&gt;
* &#039;&#039;&#039;Ein Refresh-Token gilt genau einmal.&#039;&#039;&#039; Beim Erneuern wird es entwertet. Wird es später erneut vorgelegt, gilt das als Diebstahl oder Fehlfunktion: die betroffene Sitzung wird komplett gesperrt und der Vorfall protokolliert.&lt;br /&gt;
* &#039;&#039;&#039;Alle Geräte abmelden.&#039;&#039;&#039; Ein gewöhnlicher Endpunkt kann über den Aufruf &#039;&#039;oWriter.JwtRevoke(&#039;all&#039;)&#039;&#039; auch alle Sitzungen eines Benutzers sperren - der Support-Fall „Gerät verloren&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
{{Achtung|Mit der Einführung dieser Prüfung verlieren &#039;&#039;&#039;alle vorher&lt;br /&gt;
ausgestellten Token&#039;&#039;&#039; ihre Gültigkeit, weil sie zu keiner Sitzung gehören.&lt;br /&gt;
Angemeldete Konsumenten müssen sich nach dem Update einmalig neu anmelden.}}&lt;br /&gt;
&lt;br /&gt;
Der Speicher pflegt sich selbst: Zeilen werden gelöscht, sobald sowohl das&lt;br /&gt;
Access- als auch das Refresh-Token abgelaufen sind. Details und die Skript-Seite:&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
&lt;br /&gt;
==Fehlerformat==&lt;br /&gt;
&lt;br /&gt;
Jede Fehlerantwort des Servers hat dieselbe Form: ein einziges &#039;&#039;error&#039;&#039;-Objekt&lt;br /&gt;
mit maschinenlesbarem Code, Support-Kennung, Meldung und Korrelations-ID:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;error&amp;quot;: {&lt;br /&gt;
    &amp;quot;code&amp;quot;:    &amp;quot;AUTH_EXPIRED&amp;quot;,&lt;br /&gt;
    &amp;quot;uid&amp;quot;:     &amp;quot;Y00E2RTPDY&amp;quot;,&lt;br /&gt;
    &amp;quot;message&amp;quot;: &amp;quot;Ungültiges oder abgelaufenes Token&amp;quot;,&lt;br /&gt;
    &amp;quot;traceId&amp;quot;: &amp;quot;20260817T091233123-00001A&amp;quot;&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;code&#039;&#039;&#039; - sprechender Bezeichner (z.B. &#039;&#039;AUTH_EXPIRED&#039;&#039;, &#039;&#039;NOT_FOUND&#039;&#039;, &#039;&#039;RATE_LIMITED&#039;&#039;, &#039;&#039;INTERNAL_ERROR&#039;&#039;), an dem ein Client seine Reaktion festmachen kann, ohne Texte auszuwerten.&lt;br /&gt;
* &#039;&#039;&#039;uid&#039;&#039;&#039; - OBS-Kennung der Codestelle. Für die Support-Recherche gedacht, nicht zur Auswertung im Client.&lt;br /&gt;
* &#039;&#039;&#039;message&#039;&#039;&#039; - Text für den Client. Interne Fehlerdetails stehen nie darin, sondern ausschliesslich im Protokoll &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;traceId&#039;&#039;&#039; - Korrelations-ID. Steht zusätzlich im Header &#039;&#039;X-Trace-Id&#039;&#039; und in jeder zugehörigen Logzeile.&lt;br /&gt;
&lt;br /&gt;
Ein Endpunkt-Skript kann eigene Codes setzen; siehe&lt;br /&gt;
[[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].&lt;br /&gt;
&lt;br /&gt;
{{Achtung|&#039;&#039;Fehlercode&#039;&#039;, &#039;&#039;Nachricht&#039;&#039; und &#039;&#039;traceId&#039;&#039; stehen &#039;&#039;&#039;nur&#039;&#039;&#039; im&lt;br /&gt;
&#039;&#039;error&#039;&#039;-Objekt, nicht flach daneben - der Wert von&lt;br /&gt;
&#039;&#039;Fehlercode&#039;&#039; heisst jetzt &#039;&#039;error.uid&#039;&#039;, &#039;&#039;Nachricht&#039;&#039; entspricht&lt;br /&gt;
&#039;&#039;error.message&#039;&#039;. Clients, die die flachen Felder auswerten, müssen angepasst&lt;br /&gt;
werden.}}&lt;br /&gt;
&lt;br /&gt;
==Sicherheitsmerkmale==&lt;br /&gt;
&lt;br /&gt;
Der REST-Server enthält eine Reihe von Schutzmechanismen:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Rate-Limit:&#039;&#039;&#039; Standardmässig max. 10 fehlgeschlagene und 120 erfolgreiche Anfragen pro API-Key (Account) bzw. IP und 60 Sekunden, max. 600 Anfragen global pro 60 Sekunden. Beim Überschreiten wird HTTP 429 mit &#039;&#039;Retry-After&#039;&#039;-Header zurückgegeben. Die vier Schwellen lassen sich &#039;&#039;&#039;pro Server-Profil&#039;&#039;&#039; hinterlegen; die Zähler laufen ebenfalls getrennt je Profil, ein überlastetes Profil bremst das andere also nicht mit aus. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]].&lt;br /&gt;
* &#039;&#039;&#039;Body-Limit:&#039;&#039;&#039; max. 10 MB Request-Body (JSON), max. 1024 Zeichen pro Header- oder Query-Parameter. Datei-Uploads laufen über einen separaten Pfad und sind pro Endpunkt auf &amp;lt;code&amp;gt;re_upload_size&amp;lt;/code&amp;gt; MB begrenzt (Standard 25&amp;amp;nbsp;MB).&lt;br /&gt;
* &#039;&#039;&#039;Sensible Felder&#039;&#039;&#039; (&#039;&#039;password&#039;&#039;, &#039;&#039;token&#039;&#039;, &#039;&#039;secret&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, ...) werden in Protokoll-Ausgaben automatisch maskiert.&lt;br /&gt;
* &#039;&#039;&#039;Widerrufbare Token:&#039;&#039;&#039; Jedes ausgestellte Token-Paar gehört zu einer Sitzung in &#039;&#039;&#039;RESTSRV_TOKEN&#039;&#039;&#039; und wird bei &#039;&#039;&#039;jedem&#039;&#039;&#039; Zugriff dagegen geprüft. Eine Abmeldung wirkt damit sofort und nicht erst mit dem Ablauf des Tokens; ein Refresh-Token gilt genau einmal. Siehe [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]].&lt;br /&gt;
* &#039;&#039;&#039;Reserviertes Präfix:&#039;&#039;&#039; Parameter-Namen mit Präfix &#039;&#039;_OBS_&#039;&#039; können von aussen nicht gesetzt werden, sie sind für den Server reserviert (z.B. JWT-Claims).&lt;br /&gt;
* &#039;&#039;&#039;Geblockte Header:&#039;&#039;&#039; &#039;&#039;authorization&#039;&#039;, &#039;&#039;cookie&#039;&#039;, &#039;&#039;proxy-authorization&#039;&#039;, &#039;&#039;x-forwarded-for&#039;&#039;, &#039;&#039;x-real-ip&#039;&#039;, &#039;&#039;apikey&#039;&#039;, &#039;&#039;api_key&#039;&#039; werden nicht an das Skript durchgereicht.&lt;br /&gt;
* &#039;&#039;&#039;TLS:&#039;&#039;&#039; Bei Profilen mit aktivem TLS fährt der Server ohne Zertifikat und Key nicht hoch. Plain-HTTP ist nur für ein dediziertes Debug-Profil möglich.&lt;br /&gt;
* &#039;&#039;&#039;HSTS:&#039;&#039;&#039; Antworten eines TLS-Profils tragen &#039;&#039;Strict-Transport-Security: max-age=31536000; includeSubDomains&#039;&#039;. Ein Browser spricht die Adresse danach ein Jahr lang nur noch über HTTPS an. Beim Debug-Profil &#039;&#039;Kein SSL&#039;&#039; wird der Header bewusst &#039;&#039;&#039;nicht&#039;&#039;&#039; gesetzt - der Browser würde ihn sonst speichern und die Erzwingung auch auf produktive Sitzungen anwenden.&lt;br /&gt;
* &#039;&#039;&#039;mTLS&#039;&#039;&#039; (Mutual TLS): erzwingt, dass jeder Client ein gültiges Client-Zertifikat vorlegt. Optional kann pro Zugang ein erwarteter Subject-DN hinterlegt werden.&lt;br /&gt;
* &#039;&#039;&#039;DNS-Cache:&#039;&#039;&#039; Host-zu-IP-Auflösungen für die Zugangs-Host-Prüfung werden 5 Minuten gecached.&lt;br /&gt;
&lt;br /&gt;
==Protokoll==&lt;br /&gt;
&lt;br /&gt;
Die Tabelle &#039;&#039;&#039;RESTSRV_PROTO&#039;&#039;&#039; enthält alle relevanten Ereignisse: erfolgreiche und abgelehnte Anfragen, TLS-/JWT-Fehler, Bindungs-Ereignisse, Skript-Fehler. Einträge sind nach Datum, Uhrzeit und Host (IP oder Account-Name) sortiert. Das Protokoll ist die erste Anlaufstelle bei Störungen. Jede Antwort trägt zudem eine Korrelations-ID (Response-Header &#039;&#039;X-Trace-Id&#039;&#039;), die in den Protokoll-Einträgen wiederzufinden ist.&lt;br /&gt;
&lt;br /&gt;
==Statistik==&lt;br /&gt;
&lt;br /&gt;
Die Tabelle &#039;&#039;&#039;RESTSRV_STATS&#039;&#039;&#039; enthält eine Statistik aller bearbeiteten Anfragen&lt;br /&gt;
mit IP, Zugang, Endpunkt, Methode, angefragtem Pfad, HTTP-Status und Laufzeit in&lt;br /&gt;
Millisekunden. Dazu kommen zwei Felder, die den Sprung ins Protokoll erlauben:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;rs_trace&#039;&#039;&#039; - dieselbe Korrelations-ID, die auch im Header &#039;&#039;X-Trace-Id&#039;&#039; und in jeder zugehörigen Protokollzeile steht. Damit findet man zu einer auffälligen Statistikzeile alle Protokolleinträge derselben Anfrage.&lt;br /&gt;
* &#039;&#039;&#039;rs_server&#039;&#039;&#039; - das Server-Profil, über das die Anfrage hereinkam. Bei mehreren Profilen (Public-TLS neben Internal-mTLS) ist das die einzige Angabe, an der sich der Weg ablesen lässt.&lt;br /&gt;
&lt;br /&gt;
Weil der Normalfall keine Protokollzeile mehr schreibt, ist RESTSRV_STATS die&lt;br /&gt;
vollständige Sicht auf den Betrieb - RESTSRV_PROTO die auf die Auffälligkeiten.&lt;br /&gt;
&lt;br /&gt;
Aufruf der Statistik:&lt;br /&gt;
&lt;br /&gt;
* Aus der Endpunkt-Liste: F8 zeigt die Statistik des markierten Endpunkts.&lt;br /&gt;
* Aus der Zugänge-Liste: F8 zeigt die Statistik des markierten Zugangs.&lt;br /&gt;
&lt;br /&gt;
==Konsole==&lt;br /&gt;
&lt;br /&gt;
Wird der REST-Server nicht als Dienst, sondern interaktiv gestartet, erscheint eine&lt;br /&gt;
farbige Konsole mit Live-Log und einem &#039;&#039;&#039;fixierten Kopfbereich&#039;&#039;&#039;, der die gerade&lt;br /&gt;
laufenden Anfragen zeigt und sich fortlaufend aktualisiert.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Befehl !! Wirkung&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;show &amp;lt;handle&amp;gt;&#039;&#039; || Details einer Anfrage: Request-Header, Protokollzeilen, Antwort. JSON wird farbig formatiert. &#039;&#039;&amp;lt;handle&amp;gt;&#039;&#039; ist die Kennung aus der Abschlusszeile der Anfrage (z.B. &#039;&#039;7F&#039;&#039;), &#039;&#039;&#039;keine&#039;&#039;&#039; laufende Nummer&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;1&#039;&#039; … &#039;&#039;9&#039;&#039; || Auswahl im Kopfbereich - kürzer als &#039;&#039;show&#039;&#039; für die gerade sichtbaren Anfragen. Mit &#039;&#039;mouse on&#039;&#039; auch per Klick&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;ps&#039;&#039; || Listet die gerade laufenden Anfragen auf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;level &amp;lt;stufe&amp;gt;&#039;&#039; || Anzeigestufe des Live-Logs: &#039;&#039;quiet&#039;&#039; , &#039;&#039;normal&#039;&#039; oder &#039;&#039;debug&#039;&#039;. Ohne Parameter wird die aktuelle Stufe genannt&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;filter &amp;lt;ausdruck&amp;gt;&#039;&#039; || Blendet aus, was nicht passt: &#039;&#039;path=&amp;lt;text&amp;gt;&#039;&#039;, &#039;&#039;account=&amp;lt;name&amp;gt;&#039;&#039;, &#039;&#039;status&amp;gt;=&amp;lt;code&amp;gt;&#039;&#039;. &#039;&#039;filter aus&#039;&#039; hebt die Einschränkung auf&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;panel on&amp;amp;#124;off&#039;&#039; || Fixierten Kopfbereich ein-/ausschalten. &#039;&#039;&#039;Ausgeschaltet&#039;&#039;&#039; lässt sich der Rückblick-Puffer des Konsolenfensters wieder scrollen&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;mouse on&amp;amp;#124;off&#039;&#039; || Klick-Auswahl im Kopfbereich. &#039;&#039;&#039;Eingeschaltet&#039;&#039;&#039; ist das Markieren von Text mit der Maus nicht mehr möglich&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;help&#039;&#039; / &#039;&#039;?&#039;&#039; || Zeigt diese Liste in der Konsole&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;exit&#039;&#039; / &#039;&#039;quit&#039;&#039; || Beendet den Server geordnet&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{Hinweis|Der Anzeigefilter wirkt &#039;&#039;&#039;nur auf die Anzeige&#039;&#039;&#039;. Was die Konsole&lt;br /&gt;
ausblendet, steht vollständig in RESTSRV_PROTO - es geht nichts verloren.}}&lt;br /&gt;
&lt;br /&gt;
Im Service-Modus (Windows-Dienst) ist die Konsole nicht sichtbar. Alle Einträge landen dort ausschliesslich in der Tabelle RESTSRV_PROTO.&lt;br /&gt;
&lt;br /&gt;
==Weiterführende Seiten==&lt;br /&gt;
&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Einrichtung|Einrichtung]] - Schritt-für-Schritt-Anleitung&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Server-Profile|Server-Profile]] - TLS, mTLS und Bindungen&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Zugaenge|Zugänge]] - API-Keys, JWT, CORS, Host-Beschränkung&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]] - Verwaltung, Berechtigungen, WebHooks&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]] - Aufbau der Endpunkt-Skripte&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Datei-Upload|Datei-Upload]] - Übertragungsprotokoll für Dateien (Multipart, resumable)&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Datei-Download|Datei-Download]] - Dateien ausliefern, Range/Resume, öffentliche Artefakte&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel1|Beispiel 1: Daten-Abruf mit JWT]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel2|Beispiel 2: Zeiterfassung als komplette Webanwendung]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel3|Beispiel 3: Datensatz anlegen mit JSON-Body (CRUD)]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4: Pfad-Parameter im Routing]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel5|Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz]]&lt;br /&gt;
* [[OBS/Kostenpflichtige Module/RESTServer/Beispiel6|Beispiel 6: Datei-Upload und Ablage im DMS]]&lt;/div&gt;</summary>
		<author><name>MINERVA-Rademacker</name></author>
	</entry>
</feed>