OBS/Kostenpflichtige Module/RESTServer/Scripting

Aus OBS Wiki
Zur Navigation springen Zur Suche springen
Kostenpflichtige Module

Internet-Shop
UPS
IMS Professional
SMS
Mehrlager-Verwaltung
Mehrsprachen Modul
Multilanguage Modul
EVA Marketing Tool
Termin-Projekte
Edifact-Schnittstelle
Backup Überwachung Email
OBS Geo Daten
DeliSprint / DPD
Filialen
Cashback
Moebelschnittstelle
Dokumenten Manager
DocuWare-Schnittstelle
OFML-Kalkulation
Versicherungsschaden
Gutschriftsanzeigen
Kameraverwaltung
DataInOut
OpenMasterData / IDS
Sammelpositionen


Anleitung für Endpunkt-Skripte

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.

Sicherheitshinweise

HINWEIS: Endpunkt-Skripte verarbeiten Daten aus dem Internet. Übergabeparameter dürfen niemals ungeprüft in SQL-Anweisungen eingebaut werden. Werte immer über DB_SQLVal bzw. Parameter-Bindings absichern, Eingabewerte gegen Whitelists prüfen.
  • Eingaben gegen erwartete Werte prüfen (z.B. Pflichtfelder, erlaubte Typen, Wertebereiche).
  • Nur das zurückgeben, was der Konsument wirklich braucht - keine internen IDs, keine Sys-Felder, keine Passwörter.
  • Bei Fehlern keine internen Details an den Konsumenten zurückgeben; stattdessen schreibt der Server ohnehin Detail-Einträge in RESTSRV_PROTO.

Methoden-Signatur

Pro HTTP-Methode wird im Skript eine gleichnamige Funktion implementiert. Der Server ruft genau die Funktion auf, die zur Methode des eingehenden Requests passt.

function Get   (oParams: TStrings; oBody: TJSONObject): string;
function Post  (oParams: TStrings; oBody: TJSONObject): string;
function Put   (oParams: TStrings; oBody: TJSONObject): string;
function Delete(oParams: TStrings; oBody: TJSONObject): string;
function Patch (oParams: TStrings; oBody: TJSONObject): string;

Nicht implementierte Methoden liefern automatisch 405-ähnliche Fehler über die generische Skript-Antwort.

Parameter

oParams (TStrings)

Enthält alle Query-Parameter, POST-Parameter (form-urlencoded) sowie die durchgereichten HTTP-Header. Werte sind immer Strings.

Filterung durch den Server:

  • Geblockte Header werden nicht durchgereicht: authorization, cookie, proxy-authorization, x-forwarded-for, x-real-ip, apikey, api_key.
  • Parameter mit Präfix _OBS_ können von aussen nicht gesetzt werden; sie sind für interne Werte reserviert.
  • Werte werden auf max. 1024 Zeichen begrenzt.
  • Null-Bytes und Steuerzeichen (ausser Tab, CR, LF) werden entfernt.

Zugriff im Skript:

if (not Empty(oParams.Values['kundennr'])) then begin
    cKundenNr := oParams.Values['kundennr'];
end;

oBody (TJSONObject)

Enthält den Request-Body, sofern dieser ein gültiges JSON-Objekt ist. Bei leerem oder nicht-JSON-Body wird nil übergeben.

cUser := ;
cPass := ;
if (Assigned(oBody)) then begin
    oVal := oBody.GetValue('username');
    if (Assigned(oVal)) then begin
        cUser := oVal.Value;
    end;
    oVal := oBody.GetValue('password');
    if (Assigned(oVal)) then begin
        cPass := oVal.Value;
    end;
end;

Maximale Body-Grösse: 10 MB.

Reservierte _OBS_-Parameter

Bei aktiver JWT-Authentifizierung stehen die Token-Claims als Parameter zur Verfügung:

Parameter Inhalt
_OBS_JWT_ID JWT-Id (jti-Claim) - typisch die User-Id
_OBS_JWT_SUBJECT Subject (sub-Claim) - typisch Benutzername
_OBS_JWT_AUDIENCE Audience (aud-Claim) - typisch Mandant / Rolle
_OBS_JWT_CLAIM_<name> Beliebiger Custom-Claim des Tokens (z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles); im Folge-Skript lesbar
_OBS_TRACE_ID Korrelations-ID des Requests (auch als Response-Header X-Trace-Id)

Diese Werte werden vom Server gesetzt und können vom Skript für Berechtigungs- und Mandantenprüfungen verwendet werden.

Pfad-Parameter

Stammt der Endpunkt aus einem Pfad-Template mit Platzhaltern (siehe Endpunkte), stehen die aus den Platzhaltern erfassten Werte als reservierte Parameter mit Präfix _OBS_PATH_ in oParams bereit:

Template Zugriff im Skript
/orders/{uid} oParams.Values['_OBS_PATH_uid']
/orders/{uid}/modules/{code} oParams.Values['_OBS_PATH_uid'], oParams.Values['_OBS_PATH_code']

Da der Präfix _OBS_ für von aussen gelieferte Header- und Query-Parameter gesperrt ist, sind diese Werte nicht durch den Client fälschbar. Ein vollständiges Beispiel zeigt Beispiel 4 - Pfad-Parameter.

Datei-Uploads

Ist der Endpunkt für Uploads freigeschaltet (re_upload = 1, siehe Endpunkte), nimmt der Server hochgeladene Dateien entgegen, legt sie in einem temporären Verzeichnis ab und übergibt dem Skript den Pfad über reservierte Parameter. Das Skript entscheidet selbst über die weitere Verarbeitung (z.B. DMS-Ablage). Der Body wird in diesem Fall nicht als JSON geparst - oBody ist nil. Es gilt nicht das JSON-Body-Limit (10 MB), sondern die pro Endpunkt konfigurierte Grösse (re_upload_size, Standard 25 MB; Überschreitung -> 413).

Parameter Inhalt
_OBS_UPLOAD_PATH Vollständiger Pfad zur temporären Datei auf dem Server
_OBS_UPLOAD_NAME Ursprünglicher Dateiname (nur bei multipart/form-data)
_OBS_UPLOAD_CONTENTTYPE Content-Type der Datei laut Client (nur bei multipart/form-data)
HINWEIS: Die temporäre Datei wird nicht automatisch verschoben. Das Skript muss sie an ihren Zielort (DMS, Verzeichnis, ...) übernehmen.

Einfacher Upload (multipart/form-data)

Eine Datei pro Request. Der Server speichert die erste Datei-Partie und ruft das Skript mit den drei Parametern auf:

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
    cPfad: string;
    cName: string;
begin
    oRes := TJSONObject.Create();
    try
        cPfad := oParams.Values['_OBS_UPLOAD_PATH'];
        cName := oParams.Values['_OBS_UPLOAD_NAME'];

        if (Empty(cPfad)) then begin
            oRes.AddPair('_OBS_HTTP_STATUS', 400);
            oRes.AddPair('error', 'Keine Datei empfangen');
            result := oRes.ToJSON();
            exit;
        end;

        // ... Datei aus cPfad ins DMS / Zielverzeichnis uebernehmen ...

        oRes.AddPair('status'   , 'ok');
        oRes.AddPair('dateiname', cName);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Resumable/Chunked Upload (Content-Range)

Für grosse Dateien überträgt der Client die Datei in Teilstücken (Chunks) mit dem Header Content-Range: bytes START-END/TOTAL. Der Server hängt die Chunks sequentiell an eine Session-Datei an und behandelt unvollständige Uploads selbst - das Endpunkt-Skript läuft erst beim vollständigen Upload (dann mit gesetztem _OBS_UPLOAD_PATH; _OBS_UPLOAD_NAME und _OBS_UPLOAD_CONTENTTYPE sind hier nicht gesetzt).

Ablauf (vom Client gesteuert, der Server antwortet jeweils):

Request Server-Antwort
Chunk Content-Range: bytes 0-1048575/5000000 308 Resume Incomplete + Header Upload-Id, Upload-Offset, Range
weitere Chunks (jeweils Upload-Id mitsenden) 308 mit aktualisiertem Upload-Offset
letzter Chunk (Bereich erreicht TOTAL) normale Skript-Antwort (z.B. 200/201)
Statusabfrage Content-Range: bytes */5000000 aktueller Upload-Offset, ohne anzuhängen (Resume)
Header Richtung Bedeutung
Content-Range Request bytes START-END/TOTAL; bytes */TOTAL nur als Statusabfrage
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.
Upload-Offset Response Anzahl der bereits gespeicherten Bytes (= Startoffset des nächsten Chunks)
Range Response Bereits gespeicherter Bereich (bytes=0-N)

Der Server hängt einen Chunk nur an, wenn START dem bereits gespeicherten Stand entspricht. Bei Abweichung (doppelter Chunk, Lücke) wird nicht angehängt, sondern der aktuelle Upload-Offset zurückgemeldet - der Client setzt ab dort neu auf. Eine separate Statusabfrage ist für ein Resume daher nicht zwingend nötig.

HINWEIS: Der Upload-Status liegt prozesslokal im Speicher. Nach einem Neustart des Servers muss ein unvollständiger Upload neu begonnen werden.

Rückgabe

Die Rückgabe ist immer ein JSON-String. Wird ein leerer String zurückgegeben, antwortet der Server automatisch mit {}. Der Server setzt Content-Type auf application/json; charset=utf-8 und Status auf 200, sofern das Skript nicht selbst einen Fehler signalisiert.

Einfaches Beispiel:

function Get(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        oRes.AddPair('wert_string', '123');
        oRes.AddPair('wert_int'   , 456);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Antwort steuern: Statuscode, Header, traceId

Ein Skript kann den HTTP-Statuscode und beliebige Response-Header Über reservierte Felder in der Antwort setzen. Der Server wertet sie aus und entfernt sie vor dem Senden aus dem Body.

Antwort-Feld Wirkung
_OBS_HTTP_STATUS (Zahl) HTTP-Statuscode (z.B. 201, 204, 400, 409, 422). Ohne Angabe: 200. Bei 204 wird kein Body gesendet.
_OBS_HEADERS (Objekt) Beliebige Response-Header, z.B. ETag, Location, Retry-After. CR/LF in Namen/Werten werden entfernt (Schutz vor Header-Injection).

Zusätzlich trÄgt jede Antwort den Header X-Trace-Id (Korrelations-ID). Dieselbe ID liegt dem Skript als oParams.Values['_OBS_TRACE_ID'] vor und erscheint in jeder Server-Fehler-Logzeile in RESTSRV_PROTO - so lÄsst sich ein Fehler ohne GerÄtezugriff im Log wiederfinden.

Beispiel: Anlegen mit 201, Location und ETag

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        // ... Datensatz anlegen, neue UID + Version (ETag) ermitteln ...
        oHdr := TJSONObject.Create();
        oHdr.AddPair('Location', '/v1/orders/' + cNeueUid);
        oHdr.AddPair('ETag', '1');

        oRes.AddPair('_OBS_HTTP_STATUS', 201);
        oRes.AddPair('_OBS_HEADERS', oHdr);
        oRes.AddPair('uid', cNeueUid);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Beispiel: Strukturierter Fehler mit Statuscode und traceId

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oErr: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        oErr := TJSONObject.Create();
        oErr.AddPair('code', 'VALIDATION_FAILED');
        oErr.AddPair('message', 'Feld "menge" fehlt');
        oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);

        oRes.AddPair('_OBS_HTTP_STATUS', 422);
        oRes.AddPair('error', oErr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Beispiel: Optimistic Concurrency (ETag / If-Match)

Mit Statuscode, Headern und dem lesbaren Header If-Match fÜhrt das Skript je Datensatz einen VersionszÄhler und lehnt veraltete Schreibzugriffe mit 409 ab:

function Put(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
    nAktuell, nIfMatch: integer;
begin
    oRes := TJSONObject.Create();
    try
        nAktuell := AuftragVersion(oParams.Values['_OBS_PATH_uid']);
        nIfMatch := iVal(oParams.Values['if-match']);

        if (nIfMatch <> nAktuell) then begin
            oHdr := TJSONObject.Create();
            oHdr.AddPair('ETag', xStr(nAktuell));
            oRes.AddPair('_OBS_HTTP_STATUS', 409);
            oRes.AddPair('_OBS_HEADERS', oHdr);
            oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'VERSION_CONFLICT'));
            result := oRes.ToJSON();
            exit;
        end;

        // ... speichern, Version hochzÄhlen ...
        oHdr := TJSONObject.Create();
        oHdr.AddPair('ETag', xStr(nAktuell + 1));
        oRes.AddPair('_OBS_HEADERS', oHdr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

JWT-Authentifizierungs-Skript

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 Authenticate bereitstellen:

function Authenticate(oParams: TStrings; oBody: TJSONObject): string;
var oRes  : TJSONObject;
    cUser : string;
    cPass : string;
begin
    oRes := TJSONObject.Create();
    try
        // Eingangsdaten lesen (Body oder Query-Param)
        cUser := '';
        cPass := '';
        if (Assigned(oBody)) then begin
            oVal := oBody.GetValue('username');
            if (Assigned(oVal)) then begin
                cUser := oVal.Value;
            end;
            oVal := oBody.GetValue('password');
            if (Assigned(oVal)) then begin
                cPass := oVal.Value;
            end;
        end;

        // Prüfung gegen eigene Tabelle, Hash-Verfahren, LDAP, ...
        if (PasswortPasst(cUser, cPass)) then begin
            oRes.AddPair('status'          , 1);
            oRes.AddPair('_OBS_JWT_ID'     , cUser);
            oRes.AddPair('_OBS_JWT_SUBJECT', cUser);
            oRes.AddPair('_OBS_JWT_AUDIENCE', 'mandant1');
        end else begin
            oRes.AddPair('status', 9);
            oRes.AddPair('error' , 'Login fehlgeschlagen');
        end;

        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Rückgabewerte:

status Bedeutung
1 Erfolgreich, der Server erzeugt aus den _OBS_JWT_*-Werten einen Token (HS256)
9 Misserfolg, der Server antwortet mit 403 und protokolliert den Wert von error

Custom-Claims, serverTime und Refresh

Das Authenticate-Skript kann dem Token zusÄtzlich beliebige Custom-Claims mitgeben - z.B. Mandant und Rollen - und optional ein Refresh-Token anstoßen:

RÜckgabefeld Bedeutung
_OBS_JWT_CLAIM_<name> Beliebiger Custom-Claim, z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles. Im Folge-Skript lesbar als oParams.Values['_OBS_JWT_CLAIM_<name>'].
_OBS_JWT_REFRESH_ID (optional) jti des Refresh-Tokens. Nur wenn gesetzt, stellt der Server ein Refresh-Token aus.
_OBS_JWT_REFRESH_EXP (optional) Lebensdauer des Refresh-Tokens in Minuten (Default 90 Tage = 129600).

Jedes Token trÄgt automatisch den Claim token_use (access bzw. refresh). Die Antwort enthÄlt bei Erfolg serverTime (ISO-8601 mit Offset), bei ausgestelltem Refresh zusÄtzlich refreshToken:

{"token":"...", "refreshToken":"...", "serverTime":"2026-06-29T15:30:12+02:00"}
HINWEIS: Mandant und Rollen immer aus dem Token lesen (_OBS_JWT_CLAIM_*), nie aus Body oder Query.

Refresh-Methode

Der Refresh lÄuft Über denselben JWT-Endpunkt und dasselbe Skript. Liegt am JWT-Endpunkt ein Authorization: Bearer <token> vor, ruft der Server statt Authenticate die Methode Refresh auf (sonst Login). Der Server verifiziert den Refresh-Token zuvor (Signatur, Ablauf, token_use=refresh); das alte Refresh-jti steht als oParams.Values['_OBS_JWT_ID'] bereit. Das Skript prÜft/rotiert seine Sperrtabelle und liefert wie Authenticate neue Claims zurÜck. Der Server stellt daraufhin ein rotiertes Token-Paar aus; ein bereits benutztes Refresh-Token wird mit 401 abgelehnt.

function Refresh(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
    cAltesJti, cNeuesJti: string;
begin
    oRes := TJSONObject.Create();
    try
        cAltesJti := oParams.Values['_OBS_JWT_ID'];

        // altes Refresh-jti gegen eigene Sperrtabelle prÜfen
        if (not RefreshJtiGueltig(cAltesJti)) then begin
            oRes.AddPair('status', 9);
            oRes.AddPair('error' , 'AUTH_EXPIRED');
            result := oRes.ToJSON();
            exit;
        end;

        cNeuesJti := GlobalUID();
        RefreshJtiRotieren(cAltesJti, cNeuesJti);   // altes sperren, neues ablegen

        oRes.AddPair('status'               , 1);
        oRes.AddPair('_OBS_JWT_ID'          , GlobalUID());
        oRes.AddPair('_OBS_JWT_SUBJECT'     , oParams.Values['_OBS_JWT_SUBJECT']);
        oRes.AddPair('_OBS_JWT_CLAIM_tenant', oParams.Values['_OBS_JWT_CLAIM_tenant']);
        oRes.AddPair('_OBS_JWT_CLAIM_roles' , oParams.Values['_OBS_JWT_CLAIM_roles']);
        oRes.AddPair('_OBS_JWT_REFRESH_ID'  , cNeuesJti);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Fehlerbehandlung im Skript

Tritt im Skript eine Exception auf oder schlägt die Syntax-Prüfung fehl, antwortet der Server mit 500 Interner Fehler und protokolliert die Detail-Meldung in RESTSRV_PROTO (mit Skript-Fehlertext). Der Konsument sieht keine internen Details.

Will das Skript einen spezifischen Fehler an den Konsumenten zurückgeben, ist eine eigene JSON-Struktur zu liefern:

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        if (Empty(oParams.Values['kundennr'])) then begin
            oRes.AddPair('status', 'error');
            oRes.AddPair('msg'   , 'Parameter ''kundennr'' fehlt');
            result := oRes.ToJSON();
            exit;
        end;
        // ...
    finally
        MyFreeAndNil(oRes);
    end;
end;

Skript-Cache

Der Server cached das kompilierte Skript pro Endpunkt (Schlüssel: sys_date 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.

Verfügbare Bibliotheken

Im Skript können alle OBS-Standard-Bibliotheken verwendet werden. Typische Einstiegspunkte:

  • Base.Tools - String-, Datum-, IIF-, Empty-Helper
  • Base.DB / Base.xQuery - Datenbank-Operationen, DB_SQLVal, DB_SOpen, DB_LSeek
  • Base.qSqlReg - qSqlInit, qSet, SaveData
  • System.JSON - JSON-Objekte und -Arrays
  • Base.ToolsConst - Konstanten wie CRLF, SINGELQUOTE, EMPTY_DATE

Konkrete Beispiele: