Zum Inhalt springen

Eigene Protokoll-Dekodierung

Ein privates oder hauseigenes Socket-Protokoll mitgeschnitten, das die eingebauten Formate nicht erkennen können, sodass Ihnen nichts als ein Haufen Bytes bleibt? Die eigene Dekodierung ermöglicht es Ihnen, dem Tool mit einem kurzen Skript beizubringen, wie es zu lesen ist: einen fortlaufenden Byte-Strom in einzelne Nachrichten zu zerlegen, den Protokoll-Header zu entfernen, bei Bedarf zu dekomprimieren und den Rest der automatischen strukturierten Erkennung des Tools zu überlassen.

Diese Seite richtet sich an alle, die selbst einen Decoder schreiben möchten. Sie erklärt, wie man die Funktion schreibt, wann sie aufgerufen wird und was der Rückgabewert bedeutet.


1. Was es ist: ein Hook zur frameweisen Dekodierung

Abschnitt betitelt „1. Was es ist: ein Hook zur frameweisen Dekodierung“

Sie schreiben einen Dekodierungs-Hook, und das Tool speist ihm die Bytes einer Verbindung ein und stellt wiederholt eine einzige Frage: “Kannst du, von hier ausgehend, eine vollständige Nachricht herausschneiden?” Sie schneiden eine Nachricht heraus und melden, wie viele Bytes Sie verbraucht haben; es fragt mit dem Rest erneut, und so weiter, bis der Strom vollständig zerlegt ist.

Wenn Sie jemals einen akkumulierenden Byte-Decoder in einem Netzwerk-Framework geschrieben haben, ist das Denkmodell dasselbe: bei einem fortlaufenden Byte-Strom bestimmen Sie die Nachrichtengrenzen selbst. Zwei zentrale Konventionen:

  • Sie kümmern sich nicht um die Richtung: der Sende-Strom und der Empfangs-Strom einer Verbindung laufen jeweils getrennt durch Ihren Hook, und das Tool kennzeichnet jede zerlegte Nachricht automatisch mit ihrer Richtung. Ihr Skript behandelt nur das “Wie zerlegen”.
  • Einmal zerlegt, kümmern Sie sich nicht um die Darstellung: jede Nachricht, die Sie ausgeben, durchläuft erneut die automatische Erkennung. Enthält sie protobuf, JSON oder plist, wird das weiter in eine strukturierte Form geparst. Sie sind nur für das Framing, das Entfernen von Headern und das Dekomprimieren zuständig.

Der gesamte Decoder ist eine einzige Funktion:

function decode(buf, out) {
// buf: the byte stream currently pending parse; out: the output collector; return: the number of bytes consumed this time
}

buf (Eingabe): der zu parsende Byte-Strom

  • Typ: ArrayBuffer. Sein Inhalt sind alle verbleibenden Bytes “von dort, wo Sie zuletzt aufgehört haben zu verbrauchen, bis zum aktuellen Ende”.
  • Eigenschaft buf.byteLength: wie viele Bytes gerade noch übrig sind (kann mit jedem Aufruf wachsen; so beurteilen Sie, ob genug da ist, um eine Nachricht herauszuschneiden).
  • ⚠️ Es ist ein roher Byte-Puffer, kein Uint8Array, Sie können also nicht direkt auf Bytes zugreifen (buf[0] liefert undefined). Um Bytes zu lesen, verwenden Sie die eingebauten Hilfsfunktionen aus Abschnitt 3 (u8 / u16be / sub …) oder legen Sie selbst eine Sicht darüber: new Uint8Array(buf), new DataView(buf).

out (Ausgabe): der Ausgabe-Sammler

  • Typ: ein einfaches Array.
  • Methode out.push(Nachrichten-Bytes): gibt eine Nachricht aus; Sie können in einem einzigen Aufruf mehrere ausgeben.
  • “Nachrichten-Bytes” können sein: ein von einer eingebauten Hilfsfunktion zurückgegebener ArrayBuffer (etwa sub(...), gunzip(...)) oder ein String. Andere Typen werden ignoriert.

Rückgabewert: wie viele Bytes Sie verbraucht haben

  • Typ: Ganzzahl (number), die Anzahl der Bytes, die Sie vom Anfang von buf verwendet haben.
  • > 0: Sie haben so viele Bytes vom Anfang weggenommen und eine Nachricht herausgeschnitten → das Tool verwirft sie und ruft Sie erneut mit dem Rest auf.
  • 0 / kein return / negative Zahl: bedeutet “am Anfang liegt noch keine vollständige Nachricht” oder “es lässt sich nichts mehr zerlegen” → das Tool stoppt die Schleife und zeigt die verbleibenden Bytes als roh an.
  • Der Rückgabewert wird automatisch auf die Pufferlänge begrenzt (um Überläufe zu verhindern), aber geben Sie bitte die wahre Anzahl der tatsächlich verwendeten Bytes zurück.
  • Das Tool führt einen Durchlauf über den Sende-Strom und einen über den Empfangs-Strom einer Verbindung durch. In jedem Durchlauf wird decode in einer Schleife aufgerufen: jedes Mal übergibt es Ihnen die verbleibenden, nicht verbrauchten Bytes, Sie schneiden eine Nachricht heraus und geben die verbrauchte Anzahl zurück, und so weiter, bis Sie 0 zurückgeben.
  • Eine Nachricht erscheint erst, wenn ihr Verbrauch bestätigt ist: die Nachricht, die Sie ausgeben, taucht nur auf, wenn Sie zugleich > 0 zurückgeben. Wenn Sie ausgeben, aber 0 zurückgeben, stoppt die Schleife und die Ausgabe wird verworfen, das Ausgeben einer Nachricht muss also stets mit der Rückgabe der dafür benötigten Bytezahl einhergehen.
  • Nach dem Ende der Schleife gehen nicht verbrauchte End-Bytes nicht verloren; sie werden als ein einzelnes Stück roher Daten angezeigt (zum Beispiel die zweite Hälfte einer unvollständigen Nachricht).
  • Jeder Strom (jede Richtung) erhält eine eigene, brandneue Skript-Umgebung; Sende-Strom und Empfangs-Strom vermischen sich nie.
  • Um Zustand über Aufrufe hinweg zu halten (einen Zähler, den Typ der vorherigen Nachricht und so weiter), deklarieren Sie die Variablen außerhalb von decode. Sie bleiben über Aufrufe hinweg innerhalb desselben Stroms erhalten und werden zurückgesetzt, wenn sich die Richtung ändert oder Sie neu dekodieren.
  • Die Dekodierung läuft auf bereits mitgeschnittenen Daten und kann beliebig wiederholt werden: Skript bearbeiten, speichern und erneut klicken, um dieselbe Verbindung mit den neuen Regeln neu zu dekodieren.
  • Wirft das Skript eine Ausnahme oder überschreitet ein einzelner Lauf das Zeitlimit, wird das als “diesmal nichts verbraucht” behandelt, die verbleibenden Bytes werden als rohe Daten angezeigt, und der Mitschnitt läuft weiter, ohne Datenverlust.
  • Mit anderen Worten: Das Schlimmste, was ein defektes Skript anrichten kann, ist eine fehlgeschlagene Dekodierung, die Ihnen rohe Bytes hinterlässt; die Verbindung stürzt nicht ab. Bearbeiten Sie unbesorgt.
  • Und Fehler werden nicht verschluckt: Ausnahmen und Zeitüberschreitungen werden im Debug-Ausgabe-Panel über den Ergebnissen angezeigt (siehe Abschnitt 5), gekennzeichnet, ob sie aus dem Sende-Strom oder dem Empfangs-Strom stammen. Folgen Sie ihnen direkt zur Lösung.

3. Eingebaute Hilfsfunktionen (direkt im Skript verfügbar)

Abschnitt betitelt „3. Eingebaute Hilfsfunktionen (direkt im Skript verfügbar)“

Die üblichen Aufgaben, Bytes lesen, Ganzzahlen lesen, in Text umwandeln und dekomprimieren, sind alle eingebaut, sodass Sie sie nicht neu erfinden müssen:

Kategorie Signatur Beschreibung
Teil-Ausschnitt nehmen sub(buf, off[, len]) Ab off schneiden (len weglassen, um bis zum Ende zu gehen), liefert ArrayBuffer
Ganzzahl lesen u8(buf, off) / u16be / u16le / u32be / u32le (buf, off) Vorzeichenlose Ganzzahlen in Big-Endian / Little-Endian lesen
In Text umwandeln hex(buf) / ascii(buf) In einen Hex-String umwandeln / als Text lesen
Kompression erkennen gzipMagic(buf) Ob es gzip ist (liefert einen Boolean)
Dekomprimieren gunzip / inflate / unzstd / lz4dtx (buf) Dekomprimieren; wenn es nicht geht, wird die Eingabe unverändert zurückgegeben, ohne Fehler
Debug log(...args) / console.log(...args) Ins Debug-Panel ausgeben (siehe Abschnitt 5)

Die üblichen Byte-Lese-/Schreib-Fähigkeiten (DataView, Uint8Array usw.) sind ebenfalls direkt verwendbar.


① Längenpräfix [4-Byte-Big-Endian-Länge][Nutzlast], die häufigste Gestalt eines privaten Protokolls:

function decode(buf, out) {
if (buf.byteLength < 4) return 0 // the length header hasn't fully arrived, wait
const total = 4 + u32be(buf, 0) // whole message = 4-byte header + payload
if (buf.byteLength < total) return 0 // the whole message hasn't fully arrived, wait
out.push(sub(buf, 4, total - 4)) // strip the header, hand the payload to auto recognition
return total // consume this message, go on to slice the next
}

② Nach Trennzeichen / zeilenweise, Frames an einer Marke wie einem Zeilenumbruch zerlegen:

function decode(buf, out) {
const i = ascii(buf).indexOf('\n')
if (i < 0) return 0 // no newline yet, wait
out.push(sub(buf, 0, i)) // push this line (without the newline)
return i + 1 // consume the newline along with it
}

③ Das Ganze auf einmal verarbeiten, den ganzen Strom als eine Nachricht behandeln, zum Beispiel das Ganze dekomprimieren:

function decode(buf, out) {
out.push(gzipMagic(buf) ? gunzip(buf) : buf) // if it's gzip, unpack it
return buf.byteLength // consume everything, the loop ends right away
}

④ Nach Typ verteilen, mit einem Typ-Feld im Header und unterschiedlicher Behandlung je Typ:

function decode(buf, out) {
if (buf.byteLength < 4) return 0
const total = 4 + u32be(buf, 0)
if (buf.byteLength < total) return 0
const type = u8(buf, 4) // the first byte is the message type
const body = sub(buf, 5, total - 5)
out.push(type === 2 ? gunzip(body) : body) // type 2 is compressed
return total
}

Der Editor enthält fertige Vorlagen für all diese Muster; fügen Sie eine ein, passen Sie ein paar Zahlen an, und sie läuft.

Editor für eigene Decoder: ein decode(buf, out)-Skript schreiben; “Vorlage einfügen” liefert fertige Gerüste für Längenpräfix / Magic-Signatur / Trennzeichen / feste Länge / gzip / Verteilung nach Typ, dazu eine Kurzreferenz der Funktionen


  • Schreiben und speichern: im Decoder-Editor anlegen, benennen und speichern; eine Vorlage einfügen, um loszulegen, mit den eingebauten Funktionen als Kurzreferenz griffbereit.
  • Anwenden: bei einer Verbindung, aus der Sie nicht schlau werden, rechtsklicken und “Dekodieren als” → Ihren Decoder wählen, und der gesamte Sende- und Empfangsverkehr der Verbindung wird sofort nach Ihren Regeln zerlegt und angezeigt.
  • Debuggen und Variablen prüfen: verwenden Sie innerhalb von decode log(...) oder console.log(...), um jeden Wert auszugeben: Bytezahlen, Typ-Felder, hex(sub(buf, 0, 8)) und so weiter. Die Ausgabe erscheint im Panel “Debug-Ausgabeüber den Ergebnissen von “Dekodieren als”, jede Zeile gekennzeichnet, ob sie aus dem Sende-Strom oder dem Empfangs-Strom stammt. Skriptfehler und Zeitüberschreitungen erscheinen im selben Panel, ebenfalls mit Richtung gekennzeichnet. Auf diese Weise ausgeben und lokalisieren; das ist weit schneller als blindes Bearbeiten.

    Die Sandbox hat keine vollständige Browser- / Node-console; nur console.log und das gleichwertige log sind mit diesem Panel verdrahtet. TextDecoder, fetch, setTimeout und dergleichen sind nicht verfügbar. Um Bytes als Text zu lesen, verwenden Sie ascii(buf).

  • Bearbeiten und live sehen: kein erneutes Mitschneiden nötig. Speichern Sie Ihr Skript, klicken Sie erneut auf “Dekodieren als”, und dieselbe Verbindung wird auf der Stelle mit den neuen Regeln neu dekodiert. Iterieren Sie, bis es so zerlegt, wie Sie es möchten.
  • Weiter zur Struktur: jedes dekodierte Stück wird an die automatische Erkennung zurückgegeben, sodass protobuf, JSON und plist weiter in eine strukturierte Form geparst werden, betrachtbar über die verschiedenen Ansichten in Daten prüfen und dekodieren.
  • Decoder können benannt und als Liste vorgehalten werden, um sie jederzeit hinzuzufügen, zu bearbeiten oder zu löschen.

  • Wenn Sie ein hauseigenes oder privates Socket-Protokoll mitschneiden (eine häufige Gestalt ist Längenpräfix + protobuf / JSON / Binärdaten), das die eingebauten Formate nicht erkennen können, schreiben Sie einen Decoder, um es zu zerlegen und in eine lesbare Struktur zurückzuführen.
  • Wenn Sie einen Batzen Bytes, die in einen eigenen Header eingewickelt sind oder eine Runde Kompression durchlaufen haben, wieder in lesbaren Inhalt verwandeln möchten.

Eine gebündelte Kurzreferenz: der Einstiegspunkt decode, die eingebauten Hilfsfunktionen und die Laufzeitumgebung. Jede Funktion, die buf entgegennimmt, akzeptiert entweder einen ArrayBuffer oder einen String.

decode(buf, out) → number
Zweck Der einzige Einstiegspunkt des Decoders, muss implementiert werden. Nachrichten vom Anfang von buf herausschneiden, sie per push an out geben und die diesmal verbrauchte Bytezahl zurückgeben.
buf ArrayBuffer, der verbleibende, zu parsende Byte-Strom. Verwenden Sie buf.byteLength für seine Länge; Sie können nicht direkt auf Bytes zugreifen, nutzen Sie die Hilfsfunktionen unten oder new DataView(buf) / new Uint8Array(buf).
out Array, der Ausgabe-Sammler. out.push(bytes) gibt eine Nachricht aus (bytes ist ein ArrayBuffer oder ein String; andere Typen werden ignoriert); Sie können mehrere auf einmal ausgeben.
Rückgabe number, die vom Anfang von buf verbrauchten Bytes. > 0 fährt fort; 0 / negativ / keine Rückgabe → stoppen, und verbleibende Bytes werden als roh angezeigt.
Aufruf-Konvention Je einen Durchlauf über Sende-Strom und Empfangs-Strom der Verbindung; jeder Durchlauf ruft wiederholt auf und übergibt jedes Mal die verbleibenden, nicht verbrauchten Bytes, bis Sie 0 zurückgeben.

Teil-Ausschnitt nehmen

Signatur Rückgabe Beschreibung
sub(buf, off) ArrayBuffer Von off bis zum Ende schneiden
sub(buf, off, len) ArrayBuffer len Bytes ab off schneiden; ein außerhalb des Bereichs liegendes off / len wird automatisch begrenzt, kein Fehler

Ganzzahl lesen (vorzeichenlos; liefert 0, wenn off außerhalb des Bereichs liegt)

Signatur Rückgabe Beschreibung
u8(buf, off) number 1 Byte lesen
u16be(buf, off) / u16le(buf, off) number 2 Bytes lesen, Big-Endian / Little-Endian
u32be(buf, off) / u32le(buf, off) number 4 Bytes lesen, Big-Endian / Little-Endian

In Text umwandeln

Signatur Rückgabe Beschreibung
hex(buf) string In kleingeschriebenes Hexadezimal umwandeln
ascii(buf) string Als Text auslesen (geeignet für ASCII- / Text-Inhalte)

Kompression / Dekompression (liefert die Eingabe unverändert zurück, wenn nicht dekomprimiert werden kann; Obergrenze pro Dekompression rund 16 MB)

Signatur Rückgabe Beschreibung
gzipMagic(buf) boolean Ob es mit der gzip-Magic-Zahl beginnt
gunzip(buf) ArrayBuffer gzip-Dekompression
inflate(buf) ArrayBuffer zlib- / deflate-Dekompression
unzstd(buf) ArrayBuffer zstd-Dekompression
lz4dtx(buf) ArrayBuffer LZ4-Block-Strom-Dekompression

Debug

Signatur Rückgabe Beschreibung
log(...args) (keine) Fügt die Argumente zu einer Zeile zusammen und gibt sie ins Panel “Debug-Ausgabe” aus; ein ArrayBuffer wird hexadezimal angezeigt
console.log(...args) (keine) Wie log (praktisch für alle, die console.log gewohnt sind)
  • Standard-JavaScript: gängige eingebaute Objekte wie ArrayBuffer, typisierte Arrays (Uint8Array usw.), DataView, JSON, Math, RegExp, Date sind alle verfügbar; wenn Sie byteweises Lesen/Schreiben auf niedrigerer Ebene brauchen, stehen new DataView(buf) / new Uint8Array(buf) bereit.
  • Debug: log(...) / console.log(...) geben ins Panel “Debug-Ausgabe” aus; Skriptfehler und Zeitüberschreitungen werden ebenfalls dort ausgegeben, gekennzeichnet, ob sie aus dem Sende-Strom oder dem Empfangs-Strom stammen.
  • Nicht bereitgestellt sind die übrigen browser- / Node-spezifischen Fähigkeiten: console-Methoden außer log, TextDecoder / TextEncoder, fetch, setTimeout, require usw. sind allesamt nicht verfügbar. Um Bytes als Text zu lesen, verwenden Sie ascii(buf).
  • Jeder decode-Aufruf hat eine Obergrenze für die Ausführungszeit: eine Endlosschleife oder eine Zeitüberschreitung wird unterbrochen und als “nichts verbraucht” behandelt, sodass sie das Tool nicht hängen lässt.

Zurück zu Proxy-Capture · Verwandt: Daten prüfen und dekodieren · Lokales NIC-Capture