Zum Inhalt springen

AIRLOCK

Liest eine Skill-Datei so, wie ein Assistent es täte, und sagt dir, was drinsteht.

NEXUS

Eine Windows-App, die die Teile an einem Ort hält statt in fünf Fenstern.

Skill-Index

Jeder Skill im Index, mit dem Urteil, das die Engine tatsächlich erzeugt hat.

zum Blättern zum Öffnen

Anmelden

Ein Konto ist eine E-Mail-Adresse und ein Passwort. Firebase speichert das Passwort gehasht — diese Seite sieht es nie.

Anmelden
Lizenz-API · POST · JSON

Eine Adresse, eine Methode, ein Urteil.

Die Skillry-Lizenz-API beantwortet genau eine Frage: Darf dieser Server die Skillry-Ressourcen heute noch laufen lassen? Die Ressource skillry_lizenz fragt einmal je Stunde nach — und was dabei über die Leitung geht, steht unten vollständig, samt der Stellen, an denen die Prüfung überhaupt nichts ausrichtet.

POST https://lizenz.skillry.de/v1/pruefen

Wie die Prüfung abläuft

Der Server fragt

Einmal je Stunde schickt skillry_lizenz einen POST mit dem Schlüssel und einer Handvoll Angaben über den Server. Kein Spieler sieht davon etwas: die Ressource hat kein einziges client_script, und eine Wegbeschreibung zur Lizenz-API verteilt man nicht an jeden, der sich verbindet.

Die API antwortet

JSON mit dem Feld stand: gilt oder gilt_nicht. Alles andere — ein 500er, ein Zeitablauf, ein leerer Körper, ein JSON ohne stand — ist ausdrücklich Nichtwissen und keine Absage.

Die Ressource handelt

Eine Absage wird zehn Minuten später gegengeprüft, bevor irgendetwas passiert, und angehalten wird nur, wenn C.Anhalten scharf ist. Nichtwissen führt in die Nachfrist: drei Tage, in denen nichts geschieht.


Wie ein Skript damit verbunden wird

Wer das Skillry-Paket als Ganzes betreibt, muss dafür nichts tun: skillry_lizenz hält von sich aus alles an, dessen Name mit skillry_ beginnt. Der Baustein unten ist für die beiden anderen Fälle da — für ein einzeln verkauftes Skript und für Ressourcen, die anders heißen.

1 — Der Schlüssel in die server.cfg

Der Schlüssel gehört auf den Server des Kunden und nicht in den Quelltext. Die Umgebungsvariable SKILLRY_LIZENZ_KEY leistet dasselbe — für alle, die ihre server.cfg ins Repository legen.

set skillry_lizenz_key "…"

set, nicht sets oder setr

sets schriebe den Schlüssel in die Serverinfo, wo ihn jeder Serverbrowser sieht. setr schickte ihn an jeden verbundenen Client.

2 — Die Zeile im eigenen Manifest

Diese Zeile ist Teil des Bausteins und keine Formalität. Ohne sie startet die eigene Ressource auch dann, wenn skillry_lizenz fehlt, und exports.skillry_lizenz wirft beim ersten Zugriff. Mit ihr startet sie gar nicht erst — was hier die richtige Antwort ist: eine Ressource, deren Lizenzprüfung nicht da ist, ist eine ungeprüft laufende Ressource.

dependency 'skillry_lizenz'

3 — Der Baustein in eine Server-Datei

In eine Server-Datei der Ressource, die geschützt werden soll. Nicht in eine Client-Datei und nicht in ein shared_script: die Exporte gibt es nur auf dem Server, und ein Aufruf auf der anderen Seite liefert nil — ohne Fehler und ohne Zeile im Log. Die Prüfung sähe dann aus wie eine und fände nie etwas.

--- Ein Urteil von skillry_lizenz auf die eigene Ressource anwenden.
local function skillryLizenzPruefen(stand)
  if stand and stand.stand == 'gilt_nicht' and stand.anhalten then
    StopResource(GetCurrentResourceName())
  end
end

-- Jede Aenderung — die erste Antwort ebenso wie ein Widerruf Wochen spaeter.
AddEventHandler('skillry_lizenz:stand', skillryLizenzPruefen)

-- Und einmal nachsehen, falls das Urteil schon vor dieser Ressource da war.
CreateThread(function()
  Wait(20000)
  local heil, stand = pcall(function() return exports.skillry_lizenz:Stand() end)
  if heil then skillryLizenzPruefen(stand) end
end)

Warum das Ereignis und keine Schleife

Gilt() gibt nil zurück, solange die erste Antwort aussteht — und wenn die API nicht erreichbar ist, steht sie drei Tage lang aus. Eine Startschleife liefe dann drei Tage im Sekundentakt für eine Auskunft, die es nicht gibt. Das Ereignis fliegt bei jeder Änderung, und die erste Antwort ist immer eine.

Warum die zwanzig Sekunden

Das Ereignis erreicht nicht, wer nach dem Urteil startet. skillry_lizenz wartet fünf Sekunden und dann bis zu zwei Minuten auf die Cfx-Kennung; wer früher nachsieht, erfährt nur, dass noch niemand gefragt hat, und wer später nachsieht, lässt die Ressource unnötig lange ungeprüft laufen.

Warum der pcall, obwohl die Abhängigkeit dasteht

dependency deckt den Augenblick des Startens ab und sonst nichts. Wird skillry_lizenz im laufenden Betrieb gestoppt, wirft der Export beim Zugriff — ein Fehler in der Konsole eines Kunden, verursacht von unserer Lizenzprüfung, und zwar genau dann, wenn sie ohnehin nicht antworten kann. Fällt der Blick aus, passiert nichts: das nächste Urteil holt die Ressource ein.

Was er absichtlich nicht tut

Er hält nicht an, wenn nichts bekannt ist: stand muss ausdrücklich gilt_nicht sein. if not gilt then wäre falsch, weil nil in Lua falsch ist — die Ressource hielte sich in den ersten Sekunden nach jedem Start selbst an. Er respektiert C.Anhalten, damit Stufe 2 ein Probelauf bleibt. Und er startet nichts wieder.

Die Exporte und das Ereignis

Beide gibt es nur auf dem Server. Ein Aufruf auf dem Client liefert nil, ohne Fehler und ohne Zeile im Log.

exports.skillry_lizenz:Gilt()   --> true | false | nil
exports.skillry_lizenz:Stand()  --> Tabelle, nie nil

nil ist eine von drei Antworten und kein Nein

Gilt() gibt nil zurück, solange die erste Antwort aussteht. Wer if not Gilt() then StopResource(…) schreibt, hält seine Ressource in den ersten Sekunden nach jedem Start an. true kommt auch, wenn die Prüfung ganz aus ist — wenn nicht geprüft wird, ist alles erlaubt, und ein eingebauter Baustein darf davon nichts merken.

Stand() gibt immer eine Tabelle:

{
  stand      = 'aus' | 'unbekannt' | 'gilt' | 'gilt_nicht',
  bis        = 1790000000,  -- oder nil
  seit       = 1786000000,  -- Beginn der Nachfrist
  grund      = '…',         -- oder nil
  kontakt    = '…',
  anhalten   = true,        -- ob C.Anhalten scharf ist
  angehalten = false,       -- ob es schon passiert ist
}

Das Ereignis fliegt bei jeder Änderung des Zustands, nicht bei jeder Prüfung — sonst wäre es 24-mal am Tag dieselbe Nachricht, und ein Empfänger, der daraufhin etwas tut, täte es 24-mal.

AddEventHandler('skillry_lizenz:stand', function(stand) … end)

Ein TriggerEvent und ausdrücklich kein RegisterNetEvent

Ein Netz-Ereignis mit diesem Namen könnte jeder Spieler auslösen und damit jeder Ressource erzählen, die Lizenz gelte. Eine Prüfung, die der Geprüfte selbst beantworten kann, ist keine.

Die drei Stufen des Scharfschaltens

Stufe 1 ist die Voreinstellung. Ein frisch installiertes skillry_lizenz prüft nichts und spricht mit niemandem, bis jemand es bewusst scharf schaltet — alles oben beschreibt, was nach dieser Entscheidung passiert, nicht davor.

Wer Stufe 2 überspringt, testet seine Lizenz-API zum ersten Mal an einem Server mit Spielern darauf.

StufeC.AnC.AnhaltenWas passiert
1falsefalseGar nichts. Keine Anfrage, kein Wartefaden, eine Zeile in der Konsole. Gilt() gibt true.
2truefalseEs wird geprüft und protokolliert, was passieren würde. Nichts wird angehalten; die Meldung sagt ausdrücklich dazu, dass sie folgenlos ist.
3truetrueScharf.

In Stufe 2 bleibt man, bis eine Woche Protokoll ohne falsche Absage vorliegt. Eine Woche, weil erst darin ein Wochenende steckt, ein Neustart, eine Auslieferung und eine Nacht, in der bei uns etwas kaputt war.


Der Vertrag: die Anfrage

Diese Seite ist die Abschrift, nicht das Original

Verbindlich ist die README, die neben skillry_lizenz ausgeliefert wird. Diese Seite gibt sie wieder. Zwei Beschreibungen desselben Vertrags gehen auseinander, und dann glaubt man der falschen — sollten die beiden je nicht übereinstimmen, gilt die README und nicht diese Seite.

POST https://lizenz.skillry.de/v1/pruefen

Content-Type: application/json
Accept:       application/json
User-Agent:   skillry_lizenz/<Fassung aus dem Manifest>
FeldTypBedeutung
schluesselTextDer Lizenzschlüssel. Im Körper und nicht in der Adresse — Adressen landen im Zugriffsprotokoll jedes Zwischenrechners, im Verweis-Feld und in jedem Screenshot einer Fehlermeldung. Auch nicht im Kopf: Kopfzeilen werden bei fast jedem Vorschaltserver mitprotokolliert, sobald jemand die Fehlersuche einschaltet.
kennungTextDie web_baseUrl des Servers. Je Server eindeutig, aus dem Cfx-Lizenzschlüssel abgeleitet, ohnehin öffentlich in der Serverliste — und wertlos für jeden, der sie stiehlt. Kann leer sein: beim Start noch nicht da, bei sv_lan 1 nie.
nameTextsv_hostname.
spielTextgamename, praktisch immer gta5.
plaetzeZahlsv_maxclients.
laufzeitZahlSekunden seit dem Start dieses Servers. Kein Beiwerk: eine Kennung, die immer wieder mit kleiner Laufzeit auftaucht und zwischen deren Anfragen Tage liegen, ist ein Muster und kein Zufall.
fassungTextDie Version aus dem Manifest. Damit im Protokoll steht, wer noch auf einem alten Stand läuft, ohne dass ein Kunde etwas melden muss.
lizenzTextNur wenn C.LizenzMitschicken = true: der echte sv_licenseKey. Voreingestellt aus, und das sollte so bleiben.

Nicht im Körper: die Adresse des Servers. Sie kommt aus der Verbindung, die API sieht sie selbst. Ein Feld adresse wäre eine Behauptung desselben Servers, der geprüft wird, und als Nachweis nichts wert.

kennung darf nirgends auf ein Format geprüft werden

Cfx.re stellt das Schema um und stellt der alten Kennung ein deprecated- voran. Eine Prüfung auf ^[0-9a-z]{8}\.users\.cfx\.re$ verwürfe stillschweigend jede Kennung — und damit genau das Merkmal, an dem man sieht, ob ein Schlüssel auf zwei Servern läuft. Gebraucht wird eine Zeichenkette, die je Server eindeutig ist und sich nicht ändert. Mehr muss sie nicht sein.

Der Vertrag: die Antwort

HTTP 200 mit JSON:

HTTP/1.1 200 OK
Content-Type: application/json

{ "stand": "gilt", "bis": 1790000000, "hinweis": "…", "kontakt": "…" }
FeldPflichtBedeutung
standja"gilt" oder "gilt_nicht". Alles andere wird als Nichtwissen behandelt, nicht als Absage.
bisneinUnix-Zeit, wann die Lizenz abläuft. Fehlt sie, gilt sie unbefristet. Sie ist zugleich die Obergrenze der Nachfrist: eine Lizenz, die dienstags abläuft, läuft nicht bis freitags weiter, weil montags jemand die Verbindung gekappt hat.
grundbei gilt_nichtKlartext für die Konsole des Betreibers. Er liest ihn, also ist er ein Satz und keine Fehlernummer.
kontaktneinÜberschreibt C.Kontakt. Damit bei einem Wiederverkäufer sein Kontakt in der Meldung steht und nicht unserer.
hinweisneinEine Zeile, die auch bei gilt in der Konsole erscheint — „Dein Abo läuft in 5 Tagen aus". Sie wird nur bei Änderung ausgegeben, steht also nicht 24-mal am Tag da.

Die Statuscodes

Nur zwei Dinge sind ein Nein: stand = "gilt_nicht" bei HTTP 200, und 401, 402, 403 — unbekannter oder widerrufener Schlüssel, nicht bezahlt, gesperrt.

Alles andere ist Nichtwissen, und Nichtwissen bekommt die Nachfrist: ein Zeitablauf, ein 500er, ein 404, die Fehlerseite eines Zwischenrechners, ein leerer Körper, ein JSON ohne das Feld stand. Eine Störung darf niemandem den Abend beenden.

Die eine Regel auf der Seite der API

Ein Fehler auf unserer Seite darf niemals als Absage herausgehen. Wer im Zweifel 403 antwortet, hält jeden zahlenden Server gleichzeitig an — und das ist der einzige Ausgang, der dieses Vorhaben wirklich beenden kann.

  • Eine kaputte Datenbankabfrage wird 503, nie 403.
  • Ein unlesbarer Körper wird 400, nie 401. Ein zu großer 413.
  • Zu viele Anfragen werden 429, nie 403.
  • Eine Abfrage, die nicht innerhalb von fünf Sekunden antwortet, wird 503.
  • Ein leerer Lizenzbestand wird 503, nie 401 — sonst ließe ein falscher Datenbankname oder ein leer eingespieltes Backup jeden Schlüssel unbekannt aussehen, und jeder zahlende Server hielte binnen gut einer Stunde an, ohne dass irgendetwas kaputt aussähe.
  • Fehlt die Datenbank, startet der Dienst gar nicht erst, statt hochzufahren und stumm 503 zu antworten.

Was ein Widerruf auslöst

Es gibt keinen Rückkanal — die API kann einen Server nicht anstoßen. Ein Widerruf wirkt, wenn der Server das nächste Mal fragt, also spätestens nach einer Stunde (C.TaktMinuten), plus zehn Minuten für die Gegenprobe (C.NegativTaktMinuten).

Die Gegenprobe ist der Grund, warum ein Widerruf nicht sofort wirkt, und sie ist Absicht: eine einzelne Absage kann unser Fehler sein. Eine Datenwanderung, die eine Spalte umbenennt; ein Zwischenspeicher, der die Antwort des Nachbarn ausliefert; ein Tippfehler, der alle Zeilen auf „widerrufen" setzt. In allen drei Fällen antwortet die API sauber, schnell und mit HTTP 200 — und hielte ohne Gegenprobe jeden Kunden gleichzeitig an. Ein Widerruf hat es nie eilig; ein Fehlalarm, der alle auf einmal trifft, ist ein Abend, den man nicht wiederholen will.


Was passiert, wenn die API ausfällt

Drei Tage lang nichts. Die Nachfrist läuft ab der letzten gelungenen Prüfung, nicht ab dem Serverstart des Kunden.

NachWas der Betreiber merkt
1 StundeNichts. Eine Zeile in der Konsole, sonst gar nichts. Genau das ist der Zweck — der wahrscheinlichste Ausfall, unser eigener Neustart oder unsere Auslieferung, darf nicht auffallen.
1 TagImmer noch nichts außer Konsolenzeilen. Ein Drittel der Nachfrist ist verbraucht, das Spiel läuft unverändert.
36 StundenBei jeder Prüfung eine Warnung, die die Uhrzeit nennt, zu der angehalten wird. Anderthalb Tage Vorwarnung, nicht anderthalb Sekunden.
72 StundenDie Ressourcen halten an — mit Grund und Kontakt.

Die 72 Stunden kommen nicht aus dem Gefühl. Der Fall, der die Zahl setzt, ist der Freitagabend: ein abgelaufenes Zertifikat, ein DNS-Eintrag, eine gekündigte Domain — bemerkt durch eine Kundenmeldung, behoben von einem Menschen, der schläft, arbeitet oder im Zug sitzt. Das sind knapp 60 Stunden. 72 deckt ihn mit Luft, und Luft ist hier billig.

Was beim Anhalten passiert — und was nicht

Angehalten wird, und sonst nichts. Keine versteckte Sabotage: nichts, was Datenbankzeilen verändert, Geld verschwinden lässt, Fahrzeuge löscht oder erst nach Tagen zu wirken beginnt. Ein Schaden, der nicht sofort sichtbar ist, trifft mit Sicherheit irgendwann einen zahlenden Kunden — durch einen Fehler auf unserer Seite. Und ein Skript, das im Verdachtsfall heimlich Daten verändert, macht jede Fehlersuche unmöglich, auch die eigene. Es wird auch nichts von selbst wieder gestartet: gilt die Lizenz wieder, sagt die Konsole das, und die angehaltenen Ressourcen laufen erst nach einem Neustart — ausgelöst von einem Menschen, der weiß, wer online ist.

Die ehrliche Kehrseite

Die Nachfrist wird über einen Neustart hinweg nicht gemerkt. Wer neu startet, bekommt sie neu. Das ist bekannt und hingenommen: eine Datei oder ein KVP-Eintrag auf dem Rechner des Diebes ist genauso leicht zu löschen wie diese Prüfung selbst. Ein Zustand, der nicht schützt, aber kaputtgehen kann, ist ein Zustand zu viel. Was bleibt, ist das Muster in der API — die Laufzeit in jeder Anfrage, die von der API selbst festgehaltene Absenderadresse, die Kennung, die Fassung.


Was diese Schicht nicht leistet

Sie ist eine Schwelle und kein Schloss. Dieser Abschnitt steht hier aus demselben Grund, aus dem er die README der Ressource eröffnet: er setzt die Erwartung, mit der alles Obige zu lesen ist.

Eine Lua-Datei auf einem fremden Server ist Klartext. Wer die Dateien hat, hat auch config.lua — und C.An = false ist eine Zeile, die man löscht, bevor der Kaffee kalt wird. Wer etwas gründlicher vorgeht, löscht den Baustein aus der Server-Datei; wer es eilig hat, benennt skillry_lizenz um, bis der dependency-Eintrag nicht mehr greift.

Das ist keine Schwäche dieser Umsetzung, sondern die Lage: der Server muss den Code ausführen, also muss der Server den Code haben. Jede Prüfung, die im ausgelieferten Skript steht, lässt sich mit einer geänderten Zeile umgehen. Eine Prüfung, die sich nicht umgehen ließe, müsste außerhalb des ausgelieferten Codes liegen — und dafür gibt es genau ein Werkzeug, weiter unten.

Der gemeinsame Nenner: sie wirkt auf Kunden, nicht auf Diebe. Ein Kunde, der kündigt und weiterspielt, hört auf. Ein Dieb, der die Datei liest, nicht.

Was sie dafür leistet

Wissen

Welcher Server lässt die Skripte laufen, seit wann, mit wie vielen Plätzen, auf welchem Stand. Das ist Kundendienst und Vertrieb, nicht Abwehr — und der Teil, der sich täglich auszahlt.

Ein Notausschalter

Ein Schlüssel, der widerrufen wird, hält den ehrlichen Betrieb an: bei einer Rückbuchung, einem gekündigten Abo, einer Weitergabe an einen zweiten Server.

Ein Ablaufdatum

Ein Abo ist eine Zeitangabe. Die eigene Berechtigung von Cfx.re kennt keine.

Und wenn man mehr will: Asset Escrow

Cfx.re verschlüsselt Ressourcen bei der Auslieferung. Der Kunde bekommt dann keine lesbaren Lua-Dateien, sondern eine Ressource, die nur auf einem Server läuft, dem die Berechtigung zugewiesen ist. Das ist die einzige Stufe, die einen Angreifer wirklich aufhält, und sie liegt bei Cfx.re und nicht bei uns.

Die beiden beantworten verschiedene Fragen. Escrow beantwortet „darf dieser Server den Code überhaupt ausführen" — hart, unumgehbar, und ohne jede Zeitangabe. Diese Schicht beantwortet „ist das Abo noch bezahlt, und was weiß ich über diesen Server" — weich, umgehbar, aber mit Ablaufdatum, Widerruf und Protokoll. Wer nur eines von beiden haben kann, nimmt Escrow. Wer verkauft, braucht beides: Escrow allein kann kein Abo beenden.

Der sv_licenseKey wird nicht eingesammelt

Er ist die Zugangskennung des Kunden bei Cfx.re, nicht unsere. Wer ihn hat, kann einen Server unter fremder Kennung betreiben. Ihn einzusammeln machte aus unserer Lizenz-API einen Ort, an dem sich ein Einbruch lohnt — und aus jedem Kunden jemanden, der uns etwas anvertraut hat, um das wir nie gebeten haben. Gebraucht wird er auch nicht: web_baseUrl leistet dasselbe und ist harmlos.


Fragen zur Einbindung

Frag im Forum. Ein Vertrag, den nur der Absender versteht, ist keiner.

Wen man erreicht, und wo

Der Kontakt in der Konsole kommt aus C.Kontakt oder aus dem Feld kontakt der Antwort, wenn ein Wiederverkäufer einen setzt. Für alles andere stehen die Angaben im Impressum.