JavaScript-Scripting
Chute unterstützt JavaScript-Scripting für erweiterte Anfrage-/Antwortmodifikation, benutzerdefinierte Regelabgleiche, DNS-Auflösung und geplante Aufgaben. Skripte laufen auf Apple-Plattformen auf JavaScriptCore und auf Android auf QuickJS und folgen der Surge-kompatiblen Skript-API.
Skripte werden im Abschnitt [Script] der Konfigurationsdatei definiert.
Konfiguration
[Script]
MyScript = type=http-request, script-path=/path/to/script.js, pattern=^https?://example\.com, requires-body=true, max-size=65536, timeout=10, argument=myArg
CronJob = type=cron, script-path=/path/to/cron.js, cron-expression=*/30 * * * *
Skriptparameter
| Parameter | Erforderlich | Standard | Beschreibung |
|---|---|---|---|
type |
Nein | http-request |
Skript-Trigger-Typ (siehe unten) |
script-path |
Ja | — | Lokaler Dateipfad oder HTTP(S)-URL zum JS-Skript |
pattern |
Nein | (alle) | URL-Regex-Muster zum Filtern, wann das Skript ausgelöst wird. Es wird an beliebiger Stelle der vollständigen Anfrage-URL abgeglichen — https://… nach der Entschlüsselung, http://… bei unverschlüsseltem HTTP — und nie gegen den Pfad allein, daher passt ein Muster wie ^/api nicht |
requires-body |
Nein | automatisch erkannt | Erzwingt, dass das Skript den vollständigen Anfrage-/Antwort-Body erhält |
max-size |
Nein | 131072 (128KB) | Maximale Body-Größe in Bytes für Body-zugreifende Skripte. Überschreitet der erfasste Body diese Größe, wird das Skript für diese Anfrage übersprungen (der Body wird nicht gekürzt). Bei HTTP/1.x — unverschlüsseltes HTTP und entschlüsseltes HTTP/1.1 — puffert Chute für Skripte unabhängig von diesem Wert höchstens 131072 Bytes, dort kann er das Limit also nur senken; eine entschlüsselte HTTP/2-Nachricht wird vollständig übergeben und allein gegen max-size geprüft |
timeout |
Nein | 5,0 Sekunden | Timeout pro Ausführung, gezählt ab dem Start: Das Laden des Quelltexts (bei einem Remote-Skript dessen Download), das Ausführen des Codes und das Warten auf Timer und Anfragen teilen es sich. Werte über 30 Sekunden werden auf 30 begrenzt |
argument |
Nein | — | Benutzerdefiniertes String-Argument, verfügbar als $argument in JS |
full-header-mode |
Nein | false | Bei true sind $request.headers und $response.headers statt Objekten Arrays aus {field, value}, ein Eintrag pro Header-Zeile (das Format von Surge). Gilt für http-request-, http-request-before-send- und http-response-Skripte; $done() akzeptiert in beiden Fällen beide Formen. Siehe Header mit mehreren Zeilen |
debug |
Nein | false | Reserviert; derzeit ohne Wirkung |
cron-expression |
Nein | — | Cron-Zeitplan-Ausdruck (nur für Cron-Typ); cronexp= und Shadowrockets cronexpr= werden als derselbe Schlüssel akzeptiert |
event-name |
Nein | (jedes Ereignis) | Das Ereignis, auf das ein event-Skript wartet — network-changed, engine-started oder profile-reloaded; ohne Namen läuft das Skript bei allen dreien (siehe Event-Skript) |
wake-system |
Nein | false | Reserviert; wird geparst, hat aber derzeit keine Wirkung |
enable |
Nein | true | Dieses Skript aktivieren oder deaktivieren |
script-update-interval |
Nein | 0 | Aktualisierungsabfrage für Remote-Skripte (siehe Ausführungsmodell) |
binary-body-mode |
Nein | — | Zur Surge-Kompatibilität akzeptiert (auch als binary-mode geschrieben) und beim Speichern des Profils beibehalten, ändert aber nichts: Die Rohbytes stehen immer in .bodyBytes |
engine |
Nein | — | Wird akzeptiert und beibehalten, aber ignoriert: Jedes Skript läuft auf der plattformeigenen Engine |
Skripttypen
| Typ-String | Enum | Beschreibung |
|---|---|---|
http-request |
HTTP Request | HTTP-Anfragen vor dem Upstream abfangen und ändern |
http-response |
HTTP Response | HTTP-Antworten vor dem Client abfangen und ändern |
http-request-before-send |
HTTP Request Before Send | Anfrage unmittelbar vor dem Senden an den Upstream ändern |
rule |
Rule | Benutzerdefinierte Regelabgleich-Logik |
dns |
DNS | Benutzerdefinierte DNS-Auflösung |
cron |
Cron | Geplante/zeitgesteuerte Skripte |
event |
Event | Systemereignishandler (z. B. network-changed) |
generic |
Generic | Zur Surge-Kompatibilität akzeptiert; es hängt an keinen Anfragen, Regeln, DNS-Abfragen oder Ereignissen, daher führt Chute es nie von sich aus aus. Ausführen lässt es sich mit Ausführen in der Skriptliste von Chute iOS oder Chute Android oder mit POST /api/scripts/run über die HTTP-Control-API |
Ein unbekanntes type= wird im Protokoll gemeldet und wie http-request behandelt.
Body-Autoerkennung: Bei
http-request- undhttp-response-Skripten erhält ein Quelltext, der$request.bodyoder$response.bodyenthält, den Body automatisch, bis zumax-size. Verwenden Sierequires-body=true, um dieses Verhalten zu erzwingen. Ein Skript, das nur.bodyBytesoder.rawBodyliest, wird nicht erkannt, undhttp-request-before-send-Skripte werden es nie: Diese brauchenrequires-body=true.
JavaScript-API-Referenz
Skripte laufen in einer isolierten (Sandbox-)JavaScript-Umgebung — JavaScriptCore auf Apple-Plattformen, QuickJS auf Android — mit den folgenden verfügbaren globalen Objekten.
$request (Schreibgeschützt)
Verfügbar in: http-request, http-response, http-request-before-send, rule, dns
| Eigenschaft | Typ | Beschreibung |
|---|---|---|
.url |
String | Vollständige Anfrage-URL; der Port wird ausgeschrieben, sofern er nicht der Standardport des Schemas ist, und ein IPv6-Host steht in eckigen Klammern. Ein Anfrageziel, das bereits eine absolute URL ist, wird unverändert übernommen, einem Ziel, das nicht mit / beginnt, wird eines vorangestellt, und ist kein Host bekannt, ist der Wert nur der Pfad. In einem rule-Skript ist es der Zielhost, in einem dns-Skript die abgefragte Domain |
.method |
String | HTTP-Methode (GET, POST usw.) oder QUERY für DNS |
.headers |
Object oder Array | Anfrage-Header, jeder Name mit seinem Wert; die Werte eines Headers mit mehreren Zeilen werden zu einer Zeichenkette verbunden (siehe Header mit mehreren Zeilen). Mit full-header-mode=true ein Array aus {field, value}. Namen werden unabhängig von der Groß-/Kleinschreibung gefunden (siehe Header-Namen) |
.body |
String oder null | Anfrage-Body (UTF-8 dekodiert) |
.bodyBytes |
Bytes oder null | Rohe Anfrage-Body-Bytes (siehe den Hinweis weiter unten) |
.hostname |
String | Ziel-Hostname |
.destPort |
Number | Zielport |
.processPath |
String | Pfad des anfragenden Prozesses (macOS; auf Android der APK-Pfad der App, für TCP-Verbindungen, die das VPN erfasst) |
.userAgent |
String | Wert des User-Agent-Headers |
.sourceIP |
String | Quell-IP-Adresse |
.listenPort |
Number | Port, an dem der Proxy lauscht |
.requestId |
String | Eindeutige Anfrage-ID |
.dnsResult |
String | Aufgelöste IP-Adresse |
.srcPort |
Number | Quellport |
.protocol |
String | Erkanntes Protokoll: http, https, tcp, dns |
$response (Schreibgeschützt)
Verfügbar in: http-response
| Eigenschaft | Typ | Beschreibung |
|---|---|---|
.status |
Number | HTTP-Statuscode |
.headers |
Object oder Array | Antwort-Header, jeder Name mit seinem Wert; die Werte eines Headers mit mehreren Zeilen werden zu einer Zeichenkette verbunden (siehe Header mit mehreren Zeilen). Mit full-header-mode=true ein Array aus {field, value}. Namen werden unabhängig von der Groß-/Kleinschreibung gefunden (siehe Header-Namen) |
.body |
String oder null | Antwort-Body (UTF-8 dekodiert) |
.bodyBytes |
Bytes oder null | Rohe Antwort-Body-Bytes (siehe den Hinweis weiter unten) |
.rawBody |
Bytes oder null | Alias für .bodyBytes |
.body,.bodyBytesund.rawBodytragen denselben Body: Bevor das Skript ihn sieht, wird die Chunked-Aufteilung entfernt und eine gzip- oder deflate-Content-Encodingrückgängig gemacht..bodyBytesund.rawBodyenthalten ein Byte-Objekt. Übergeben Sie es an einen Aufruf, der Bytes entgegennimmt — an$utils.ungzip(), dessen Ergebnis ebenfalls ein Byte-Objekt ist, oder anbodyin$done(), das es als rohen Body annimmt. Was das Objekt ist, hängt von der Engine ab: auf Apple-Plattformen ein undurchsichtiger Wrapper ohne.lengthund ohne Indizierung, auf Android einUint8Array; ein Skript, das für alle Plattformen gedacht ist, sollte sich daher weder auf.lengthnoch auf Indizierung verlassen. Um den Inhalt zu lesen,.bodyverwenden: dieselben Bytes, als UTF-8 dekodiert.
$done(value) — Abschluss-Handler
Muss genau einmal am Ende des Skripts aufgerufen werden, um den Abschluss zu signalisieren. Ein Skript, das zurückkehrt, ohne ihn aufgerufen zu haben, wird sofort als Durchleitung abgeschlossen — es sei denn, ein setTimeout-Timer oder eine $httpClient-Anfrage steht noch aus. timeout= läuft ab dem Moment, in dem die Ausführung beginnt: Das Laden des Quelltexts, das Ausführen des Codes und dieses Warten werden alle darauf angerechnet.
$done({}) // Durchleitung — keine Änderungen
$done() // Verbindung abbrechen
$done({matched: true}) // Regelabgleich-Ergebnis (nur Regel-Skripte)
$done({address: "1.2.3.4"}) // DNS-Ergebnis (nur DNS-Skripte)
In einem http-request-, http-request-before-send- oder http-response-Skript bricht $done() ohne Argument — oder mit einem Wert, der kein Objekt ist — die Verbindung ab: Chute sendet nichts anstelle der Anfrage oder der Antwort, und eine abgebrochene Anfrage erreicht den Server nie. Was bereits auf dem Weg zum Client ist, wird zuerst geschrieben, dann wird die Verbindung sofort geschlossen. Bei HTTP/2 wird nur der Stream dieser Anfrage zurückgesetzt (RST_STREAM mit dem Fehlercode CANCEL), und nichts wird an seiner Stelle gesendet; die Verbindung und die anderen Anfragen darauf laufen weiter.
Header und url aus einem Skript werden vor der Verwendung geprüft. Ein Header wird mit einer Warnung im Protokoll verworfen, wenn:
- sein Name oder Wert einen Zeilenumbruch (CR oder LF) oder ein NUL-Zeichen enthält:
[JS] Dropped header <name>: a header name or value cannot contain a line break; - sein Name kein gültiger HTTP-Feldname ist (RFC 9110 §5.1). Ein gültiger Name besteht aus einem oder mehreren Buchstaben, Ziffern oder Zeichen aus
!#$%&'*+-.^_`|~; ein leerer Name oder einer mit Leerzeichen, Doppelpunkt oder einem Nicht-ASCII-Buchstaben wird daher verworfen:[JS] Dropped header <name>: not a valid header name.
Der Rest des Ergebnisses gilt weiter. Dieselben Prüfungen gelten für Header in der Array-Form und für die Header einer response, die ein Anfrage-Skript zurückgibt. Eine url mit Zeilenumbruch, NUL oder Leerzeichen wird ignoriert, ebenfalls mit einer Warnung. Pfad und Query einer url werden so gesendet, wie sie geschrieben sind: Prozent-Escapes wie %20 bleiben erhalten.
Rückgabewerte für HTTP-Request-Skripte:
$done({
url: "https://new.example.com/path", // URL umschreiben
headers: {"X-Custom": "value"}, // Header ändern
body: "new request body", // Body ändern
response: { // Synthetische Antwort zurückgeben (Upstream überspringen)
status: 200,
headers: {"Content-Type": "text/html"},
body: "<html>Blocked</html>"
}
})
Wenn response angegeben ist, wird die Anfrage kurzgeschlossen: Chute gibt die synthetische Antwort direkt an den Client zurück, und die Anfrage wird nicht an den Upstream-Server gesendet. Das gilt überall, wo Chute die Anfrage verarbeitet, HTTP/2 eingeschlossen, und unabhängig davon, ob das Skript am Header, am vollständigen Body oder als http-request-before-send läuft. Bei HTTP/1.x trägt die Statuszeile den Standard-Statustext (Reason Phrase) des Codes (HTTP/1.1 404 Not Found), und die Verbindung wird geschlossen, sobald die Antwort draußen ist; bei HTTP/2 wird nur der Stream dieser Anfrage beantwortet. status ist eine Zahl von 200 bis 599 oder eine Zeichenkette, die nach dem Entfernen umgebender Leerzeichen eine solche ganze Zahl ist ("404"); jeder andere Wert oder keiner ergibt 200. Content-Length wird aus body berechnet, egal was headers sagt. Dies ist nützlich zum Blockieren, Mocken von APIs oder Zurückgeben von zwischengespeicherten Inhalten.
Antworten, die Chute selbst erzeugt — Map Local, die
responseeines Skripts sowie die Antworten und die Fehlerseite der URL-Umschreibung und der REJECT-Richtlinien — folgen HTTP: Die Antwort auf eine HEAD-Anfrage besteht nur aus dem Header, mit der Content-Length, die eine GET-Anfrage bekäme, und eine Antwort mit Status 204, 205 oder 304 trägt weder einen Body noch eine Content-Length.
Rückgabewerte für HTTP-Response-Skripte:
$done({
status: 200, // Statuscode ändern
headers: {"X-Custom": "value"}, // Antwort-Header ändern
body: "new response body", // Antwort-Body ändern
url: "https://other.example.com" // Weiterleitung (standardmäßig 302)
})
body ersetzt den Body der Antwort und sonst nichts: Statuszeile und Header bleiben die des Servers, mit dem zurückgegebenen status und den headers eingearbeitet, und der eigene Body des Servers wird verworfen. Lief das Skript nur am Header, laufen danach die Regeln der Body-Umschreibung und die http-response-Skripte, die den Body erhalten, wie gewohnt auf dem neuen Body. Eine Antwort auf HEAD oder eine mit Status 1xx, 204, 205 oder 304 hat keinen Body; ein zurückgegebener body wird dort ignoriert, status und headers gelten aber weiterhin.
status ist eine Zahl von 200 bis 599 oder eine Zeichenkette, die nach dem Entfernen umgebender Leerzeichen eine solche ganze Zahl ist ("301"). Jeder andere Wert, auch true und false, wird ignoriert, und die Antwort behält ihren Status. Ändert status den Code, erhält die Statuszeile auch den Standard-Statustext (Reason Phrase) des neuen Codes. Ein status von 204, 205 oder 304 schickt die Antwort ohne Body — weder den des Servers noch einen zurückgegebenen — und ohne Längen-Header; bei HTTP/1.x wird die Verbindung geschlossen, sobald der Header draußen ist.
Wenn url angegeben ist, antwortet Chute anstelle der ursprünglichen Antwort mit einer Weiterleitung dorthin. Ihr Status ist der zurückgegebene status — etwa 301, 307 oder 308 — oder 302, wenn keiner angegeben ist oder er ignoriert wird; die zurückgegebenen headers werden wie gewohnt eingearbeitet, und Location ist url. Ohne body hat die Weiterleitung keinen, und der Body des Servers wird nie gesendet; bei HTTP/2 werden auch die Trailer des Servers nicht gesendet. url muss zusammen mit mindestens einem von status / headers / body zurückgegeben werden; ein Ergebnis, das nur url enthält, wird als Durchleitung behandelt.
Rückgabewerte für DNS-Skripte:
$done({address: "1.2.3.4"}) // Einzelne IP
$done({addresses: ["1.2.3.4", "5.6.7.8"]}) // Mehrere IPs
$done({address: "10.0.0.1", ttl: 300}) // Mit benutzerdefinierter TTL (Sekunden, Standard 60)
$done({server: "1.1.1.1"}) // Über diesen Server auflösen
$done({servers: ["tls://dns.google", "8.8.8.8#disable-ipv6"]})
server/serversantworten nicht, sie wählen den Resolver. Jeder Eintrag ist ein gewöhnlicher DNS-Server-Eintrag — eine Adresse,tls://,https://, mit den üblichen#-Optionen — und sie werden gemeinsam gefragt; die erste Antwort gewinnt. Ist kein Eintrag brauchbar, scheitert die Auflösung, statt auf den konfigurierten Pool zurückzufallen.
Header mit mehreren Zeilen
Ein Header kann in mehreren Zeilen vorkommen: Eine Antwort, die zwei Cookies setzt, trägt zwei Set-Cookie-Zeilen. http-request-, http-request-before-send- und http-response-Skripte können jede Zeile lesen und mehrere schreiben.
Lesen
Standardmäßig sind $request.headers und $response.headers Objekte, die jedem Header-Namen eine Zeichenkette zuordnen. Hat ein Header mehrere Zeilen, werden ihre Werte der Reihe nach verbunden: die von Cookie mit "; ", die jedes anderen Headers (auch Set-Cookie) mit ", ".
$response.headers["Set-Cookie"] // "a=1; Path=/, b=2; Path=/"
$request.headers["Cookie"] // "a=1; b=2"
Das Expires-Datum eines Cookies enthält ein Komma, daher lässt sich eine verbundene Set-Cookie-Zeichenkette nicht zuverlässig wieder in Cookies zerlegen. Um jede Zeile einzeln zu lesen, fügen Sie der Skriptzeile full-header-mode=true hinzu. $request.headers und $response.headers sind dann Arrays mit einem {field, value}-Objekt pro Zeile, dem Format von Surge. Zeilen mit demselben Namen behalten ihre Reihenfolge.
// full-header-mode=true
const cookies = $response.headers
.filter(h => h.field.toLowerCase() === "set-cookie")
.map(h => h.value) // ["a=1; Path=/", "b=2; Path=/"]
Dasselbe gilt über HTTP/2: Teilt ein Client sein Cookie auf mehrere cookie-Felder auf, liest das Skript sie als ein Cookie, verbunden mit "; ", und jedes set-cookie ist eine eigene Zeile.
Schreiben
headers in $done() ändert nur die Header, die es nennt; jeder andere Header bleibt, wie er ist. Dieselben Regeln gelten für headers in einer response, mit der ein Anfrage-Skript antwortet.
| Wert | Wirkung |
|---|---|
| eine Zeichenkette | Der Header erhält genau eine Zeile mit diesem Wert; alle bisherigen Zeilen werden ersetzt |
| ein Array aus Zeichenketten | Eine Zeile pro Wert, in dieser Reihenfolge, anstelle der Zeilen des Headers. {"Set-Cookie": ["a=1", "b=2"]} sendet zwei Set-Cookie-Zeilen |
[] |
Der Header wird entfernt |
alles andere (eine Zahl, null, ein Objekt, ein Array mit einem Element, das keine Zeichenkette ist) |
Ignoriert: Der Header bleibt, wie er ist |
Gibt ein Skript eine verbundene Zeichenkette genau so zurück, wie es sie gelesen hat, behält der Header seine ursprünglichen Zeilen. So ändert const h = $response.headers; h["X-A"] = "1"; $done({headers: h}) nur X-A, und zwei Set-Cookie-Zeilen gehen weiterhin als zwei Zeilen hinaus. Jede andere Zeichenkette ersetzt alle Zeilen des Headers durch eine. Um ein Cookie hinzuzufügen, schreiben Sie die vollständige Liste als Array: Lesen Sie die vorhandenen Zeilen im Full-Header-Modus und geben Sie dann {"Set-Cookie": [...existing, "c=3"]} zurück.
Eine Anfrage trägt eine einzige Cookie-Zeile: Ein Array, das ein http-request- oder http-request-before-send-Skript in headers für Cookie angibt, wird mit "; " verbunden, {"Cookie": ["a=1", "b=2"]} sendet also Cookie: a=1; b=2.
$done() akzeptiert headers auch als Array aus {field, value}-Objekten, mit oder ohne full-header-mode:
$done({headers: [{field: "Set-Cookie", value: "a=1"},
{field: "Set-Cookie", value: "b=2"},
{field: "X-A", value: "1"}]})
Die Einträge werden nach Namen gruppiert, ohne Beachtung der Groß-/Kleinschreibung, und jede Gruppe ersetzt die Zeilen dieses Headers in der angegebenen Reihenfolge. Header, die das Array nicht nennt, bleiben, wie sie sind; einen Header wegzulassen entfernt ihn also nicht, dafür dient die Objektform mit []. Ein Eintrag ohne field und value als Zeichenketten wird ignoriert.
- Header-Namen unterscheiden nicht zwischen Groß- und Kleinschreibung:
set-cookieundSet-Cookiesind derselbe Header, geben Sie jeden Namen also nur einmal an. Ein Name, der kein gültiger Header-Name ist, wird verworfen, wie unter$donebeschrieben. - Ein Wert mit Zeilenumbruch oder NUL wird verworfen, wie unter
$donebeschrieben. In einem Array wird nur dieser Wert verworfen; werden alle verworfen, bleibt der Header, wie er ist. - Bei HTTP/2 wird jede Zeile zu einem eigenen Feld mit kleingeschriebenem Namen. Header, die nur für eine Verbindung gelten, etwa
ConnectionundKeep-Alive, werden nicht gesendet.
Header-Namen in $request.headers und $response.headers
$request.headers und $response.headers finden einen Header unabhängig davon, in welcher Schreibweise sein Name angegeben wird: $response.headers['ETag'], $response.headers['etag'] und $response.headers['Etag'] lesen alle denselben Header. Das gilt auch für in ('etag' in $response.headers), für Zuweisungen (headers['content-type'] = 'text/html' ändert den Content-Type, der schon im Objekt steht, statt einen zweiten Namen dafür anzulegen) und für delete.
Wenn Sie die Namen auflisten (Object.keys(), for…in, JSON.stringify()), erscheint jeder Header einmal, in der Schreibweise, in der Chute ihn speichert — jedes Wort großgeschrieben, egal wie Client oder Server ihn geschrieben haben: Content-Type, Etag, X-Api-Key, Www-Authenticate.
Nur das Objekt, das Sie aus $request.headers oder $response.headers lesen, gleicht Namen so ab. Eine Kopie, die Sie selbst anlegen, etwa mit Object.assign({}, $request.headers) oder {...$response.headers}, ist ein gewöhnliches Objekt; verwenden Sie in einer Kopie die oben gezeigte Schreibweise. Mit full-header-mode=true ist headers ein Array aus {field, value}-Objekten, field in derselben Schreibweise, und dieser Abgleich gilt nicht; vergleichen Sie stattdessen field.toLowerCase().
$httpClient — Asynchroner HTTP-Client
Führen Sie HTTP-Anfragen aus Skripten heraus aus. Alle Anfragen werden bei $done() oder Timeout abgebrochen.
$httpClient.get(url, function(error, response, data) {
if (error) {
console.log("Anfrage fehlgeschlagen: " + error)
} else {
console.log("Status: " + response.status)
console.log("Antwort: " + data)
}
})
$httpClient.post(url, {headers: {...}, body: "...", timeout: 5}, callback)
$httpClient.put(url, options, callback)
$httpClient.del(url, options, callback)
$httpClient.head(url, options, callback)
$httpClient.options(url, options, callback)
$httpClient.patch(url, options, callback)
Ein Optionsobjekt kann außerdem policy tragen, den Namen einer im Profil definierten Richtlinie: Die Anfrage wird dann über diese Richtlinie aufgebaut statt über die Standardroute. Ein Name, den das Profil nicht definiert, wird als Warnung protokolliert, und die Anfrage nimmt die Standardroute. Surges Inline-Form policy-descriptor wird nicht unterstützt: Sie wird als Warnung protokolliert, und die Anfrage nimmt ebenfalls die Standardroute — definieren Sie die Richtlinie im Profil und übergeben Sie ihren Namen.
Callback-Signatur: callback(error, response, data)
error: Fehler-String odernullresponse:{status: Number, headers: Object}odernull.headersfindet einen Header unabhängig von der Schreibweise seines Namens und listet die Namen so wie$request.headers— jedes Wort großgeschrieben, etwaContent-Type,Etag,X-Api-Key—, mit oder ohnepolicydata: der als UTF-8-String dekodierte Antwort-Body; ein Body, der kein gültiges UTF-8 ist, kommt wie.bodyBytesals Byte-Objekt an, nicht alsnull.nullnur dann, wenn es gar keinen Body gibt
$httpClientist inhttp-request-,http-response-,http-request-before-send-,cron- undevent-Skripten verfügbar. Inrule- unddns-Skripten ist es nicht verfügbar.Ein Skript darf höchstens 8 ausstehende Anfragen haben, alle Skripte zusammen 16. Eine Anfrage, die eine der beiden Grenzen überschreitet, wird nicht gesendet, und ihr Callback läuft nie; das Protokoll meldet das einmal pro Lauf. Ein Antwort-Body über 4 MB lässt die Anfrage fehlschlagen.
$persistentStore — Schlüssel-Wert-Speicher
Persistenter Schlüssel-Wert-Speicher, der Skript- und Prozessneustarts überdauert; jedes Skript liest und schreibt dieselben Schlüssel. Auf Android wird er im privaten Speicher von Chute aufbewahrt und von Gerätesicherungen ausgenommen.
$persistentStore.write(data, key) // Einen Wert speichern
$persistentStore.read(key) // Einen Wert abrufen
$persistentStore.remove(key) // Einen Wert entfernen
$notification — Lokale Benachrichtigungen
Lokale Systembenachrichtigungen senden. Chute Apple TV zeigt keine an.
$notification.post("Titel", "Untertitel", "Benachrichtigungstext")
Ein optionales viertes Argument trägt Surges Optionen: url (der Link, der beim Tippen auf die Benachrichtigung geöffnet wird), action (open-url ergibt sich aus url und ist die einzige Aktion, auf die die Apps reagieren — andere, etwa Surges clipboard, werden mitgeführt, aber nichts wertet sie aus) und auto-dismiss (Sekunden). Sie werden der Benachrichtigung angehängt; andere Schlüssel werden mit einer Warnung ignoriert. url wird von Chute iOS, Chute Mac und Chute Android ausgewertet, die es beim Klick auf die Benachrichtigung öffnen. auto-dismiss wird nur von Chute Android ausgewertet, das die Benachrichtigung nach so vielen Sekunden entfernt; die Apple-Apps lassen sie stehen.
$notification.post("Title", "", "Tap to open", {url: "https://example.com", "auto-dismiss": 5})
Skript-Benachrichtigungen folgen — wie die von Regeln mit notification-text — dem Benachrichtigungsschalter der App: Benachrichtigung erlauben in Chute iOS, Ereignisbericht-Benachrichtigungen anzeigen in Chute Mac, die Benachrichtigungsberechtigung des Systems für Chute auf Android. Schalten Sie ihn aus, erscheint nichts. Chute Android sendet sie über einen eigenen Benachrichtigungskanal, Skript-Benachrichtigungen, der sich in den Benachrichtigungseinstellungen des Systems separat ausschalten lässt. Siehe Benachrichtigungsberichte.
$network — Netzwerkinformationen
Schreibgeschützte Netzwerkzustandsinformationen.
$network.dns // Array von DNS-Server-IPs
$network.wifi // {ssid: "WiFiName", bssid: null}
$network.v4 // {primaryAddress, primaryRouter}
$network.v6 // {primaryAddress, primaryRouter}
$network.primaryRouter // IPv4 default gateway, or null
$network.cellularData // {radio: "LTE" | "5G" | ..., carrier: null}
ssidundbssidwerden unter iOS befüllt, wo der Tunnel die WLAN-Identität lesen darf, und unter Android, wenn Chute die Berechtigung besitzt, die Android zum Lesen des Netzwerknamens verlangt (siehe Erste Schritte auf Android); unter macOS und tvOS istssidleer undbssidnull.primaryAddressüberspringt die eigene Adresse des Tunnels und Link-Local-Adressen und ist damit die Adresse, mit der das Gerät das Netz erreicht.carrierist immernull— iOS 16 hat den Namen des Mobilfunkanbieters entfernt.
$environment — Laufzeitinformationen
$environment.system // "iOS", "macOS" oder "Android"
$environment.appVersion // KLNEKit-SDK-Versionsstring
$environment.surgeVersion // Version der Engine selbst, unter dem Namen, den Surge-Skripte lesen
$environment.language // Preferred language as a BCP 47 tag, e.g. "en-US"
$environment.deviceModel // "iPhone", "Mac" or "AppleTV"; on Android the device's model, e.g. "Pixel 8"
$utils — Hilfsfunktionen
$utils.geoip("1.2.3.4") // Ländercode (z. B. "US")
$utils.ipasn("1.2.3.4") // ASN-Nummer (z. B. "13335")
$utils.ipaso("1.2.3.4") // Organisation des ASN (z. B. "CLOUDFLARENET")
$utils.ungzip(data) // Gzip-Daten dekomprimieren
$klne — Proxy-Control-API
Steuern Sie die Proxy-Laufzeit aus Skripten. $klne ist in jedem Skripttyp verfügbar.
$klne.getPolicyGroups() // Alle Richtliniengruppen abrufen
$klne.selectGroupDetails() // Dieselben Gruppen, in Surges Form
$klne.selectPolicy("Group", "Proxy") // Richtlinie für eine Gruppe wechseln
$klne.getActiveConnections() // Aktive Verbindungen auflisten
$klne.closeConnection("id") // Schließt die Verbindung mit der von getActiveConnections() gemeldeten id
$klne.flushDNS() // DNS-Cache leeren
$klne.startURLTest("Group") // URL-Test für eine Gruppe auslösen
$klne.reloadConfiguration() // Gesamte Konfiguration neu laden
$klne.setOutboundMode("rule") // Modus setzen: "global"/"proxy", "direct", "rule"
$klne.setHTTPCaptureEnabled(true) // MitM aktivieren/deaktivieren
$klne.setRewriteEnabled(true) // Umschreibungsfamilie aktivieren/deaktivieren
getPolicyGroups() gibt die auswählbaren Richtliniengruppen zurück:
{
count: 1, // Number — Anzahl der Gruppen
policyGroups: [{
name: "MainGroup", // String — Gruppenname
type: 0, // Number — 0 select, 1 url-test, 2 fallback, 3 ssid, 5 load-balance
policyNames: ["A", "B"], // Array of String — die tatsächlichen Mitglieder der Gruppe, Knoten aus Abonnements eingeschlossen
selectedIndex: 0, // Number — Index der aktiven Richtlinie in policyNames (fehlt, wenn nicht ermittelbar)
selectedPolicy: "A" // String — Name der aktiven Richtlinie (fehlt, wenn nicht ermittelbar)
}]
}
selectGroupDetails() gibt dieselben Gruppen in Surges Form zurück — policyGroups ordnet jedem Gruppennamen die Namen seiner Mitglieder zu, decisions jedem Gruppennamen das gerade ausgewählte Mitglied:
{
policyGroups: {MainGroup: ["A", "B"]},
decisions: {MainGroup: "A"}
}
Eine Gruppe, deren Auswahl nicht ermittelbar ist, fehlt in decisions.
getActiveConnections() gibt ein Array zurück, das die aktuellen Verbindungen beschreibt:
[{
id: 1042, // Number — die Verbindungsnummer, die closeConnection annimmt
host: "example.com", // String — Zielhost (fehlt, wenn unbekannt)
port: 443 // Number — Zielport
}]
Die übrigen Methoden nehmen einfache Argumente entgegen und geben nichts zurück:
selectPolicy(group, policy)— machtpolicy(einen Namen aus denpolicyNamesder Gruppe) zur aktiven Richtlinie vongroup. Ein unbekannter Gruppen- oder Richtlinienname wird mit einer Warnung im Protokoll ignoriert.closeConnection(id)— schließt die Verbindung mit dieser id, der Nummer, diegetActiveConnections()meldet. Eine nicht offene id wird mit einer Warnung ignoriert.flushDNS()— leert den DNS-Cache.startURLTest(group)— startet einen asynchronen Latenztest für eineurl-test-,fallback- oderload-balance-Gruppe; andere Gruppentypen und unbekannte Namen werden mit einer Warnung ignoriert.reloadConfiguration()— holt die#!MANAGED-CONFIG-Quelle des Profils sofort erneut, statt das Update-Intervall abzuwarten, und wendet sie an, falls sie sich unterscheidet. Die Konfiguration, die die Engine ohnehin hält, erneut anzuwenden ändert nichts; ein Profil ohne verwaltete Quelle protokolliert daher nur eine Warnung.setOutboundMode(mode)—"global"und"proxy"leiten beide den gesamten Verkehr über den Proxy,"direct"sendet den gesamten Verkehr direkt, und jeder andere Wert wählt den Regelmodus.setHTTPCaptureEnabled(enabled)— aktiviert oder deaktiviert die HTTPS-Entschlüsselung (MitM) zur Laufzeit; nimmt einen Boolean entgegen.setRewriteEnabled(enabled)— aktiviert oder deaktiviert zur Laufzeit die gesamte Umschreibungsfamilie — URL-Umschreibung, Header-Umschreibung, Body-Umschreibung und Mock-Antwort; nimmt einen Boolean entgegen.
Für Surge-Skripte gibt es $surge, mit den Aufrufen, deren Bedeutung identisch ist: $surge.setSelectGroupPolicy(group, policy) (wie selectPolicy), $surge.selectGroupDetails() (wie selectGroupDetails), $surge.setOutboundMode(mode), $surge.setHTTPCaptureEnabled(enabled), $surge.setRewriteEnabled(enabled) (die gesamte Umschreibungsfamilie: URL-Umschreibung, Header-Umschreibung, Body-Umschreibung und Mock-Antwort) und $surge.retestGroup(name). Der Rest von Surges $surge hat hier keine Entsprechung und liest sich als undefined, worauf ein Skript prüfen kann.
$httpAPI — Brücke zur Control-API
Ruft die HTTP-Control-API der Engine aus einem Skript auf. $httpAPI ist in jedem Skripttyp verfügbar.
$httpAPI("GET", "/api/status", null, function(result) {
console.log(result.statusCode) // Number — HTTP-Status
console.log(result.body.data) // Object — der geparste JSON-Body
})
$httpAPI("/api/status") // Ein Argument: ein GET auf diesen Pfad
$httpAPI("DELETE", "/api/dns/cache") // Methode und Pfad
var result = $httpAPI("GET", "/api/status") // Dasselbe Objekt wird auch zurückgegeben
Der Aufruf ist synchron — die Anfrage wird innerhalb der Engine geroutet, und das {statusCode, body}-Objekt wird sowohl an den Callback übergeben als auch zurückgegeben. path muss mit / beginnen. Ein body vom Typ String wird unverändert gesendet; jeder andere Wert wird als JSON kodiert. Da die Anfrage den Listener nie passiert, ist kein Token im Spiel, und die API muss nicht aktiviert sein. POST /api/scripts/run wird mit 409 und would_reenter abgelehnt — es bräuchte die Engine, die das aufrufende Skript gerade hält — und eine Route, die nicht binnen 12 Sekunden antwortet, ergibt 504.
$script — Skript-Metadaten
$script.name // Skriptname aus der Konfiguration
$script.type // Skripttyp-String
$script.startTime // Monotoner Zeitstempel (Sekunden seit System-Boot-Referenz, nicht Epoche)
$script.sessionID // Pro Ausführung verschieden, um den Zustand eines Laufs zusammenzuhalten
Globale Variablen pro Ausführung
Die folgenden Variablen werden pro Skriptausführung injiziert und sind spezifisch für bestimmte Skripttypen.
$argument — Skript-Argument
Der String-Wert aus dem Parameter argument= in der Skriptkonfiguration oder null, wenn das Skript keinen hat. Verfügbar in: http-request, http-response, http-request-before-send, rule, dns, cron, event.
console.log("Argument: " + $argument)
$domain — DNS-Domain (Nur DNS-Skript)
Der abgefragte Domainname. Nur in dns-Skripten verfügbar.
var domain = $domain // z. B. "example.com"
$cronexp — Cron-Ausdruck (Nur Cron-Skript)
Der Cron-Zeitplan-Ausdruck aus der Skriptkonfiguration. Nur in cron-Skripten verfügbar.
console.log("Zeitplan: " + $cronexp) // z. B. "*/30 * * * *"
$event — Ereignisinformationen (Nur Event-Skript)
Informationen über das auslösende Ereignis. Chute löst network-changed, engine-started und profile-reloaded aus — siehe Event-Skript.
console.log("Ereignis: " + $event.name) // "network-changed"
$event.name ist das Ereignis, das tatsächlich ausgelöst hat. Ein Skript mit event-name= läuft immer nur für dieses eine Ereignis, der Name ist also stets der deklarierte; ein Skript ohne event-name= läuft bei allen dreien, und $event.name ist der Weg, sie auseinanderzuhalten.
console — Protokollierung
console.log("Debug-Nachricht") // Ausführliche Protokollierung
console.warn("Warnmeldung") // Warnprotokoll
console.error("Fehlermeldung") // Warnprotokoll, als JavaScript-Fehler gekennzeichnet
Jede Nachricht wird auf 256 Zeichen gekürzt, und Zugangsdaten darin — ein Authorization- oder Cookie-Header, ein password=- oder token=-Wert und dergleichen — werden als <redacted> protokolliert. console.log schreibt auf der Stufe verbose und erscheint daher nur, wenn loglevel auf verbose steht; console.warn und console.error erscheinen auf der Standardstufe warning.
setTimeout(fn, seconds) — Timer
Plant die Ausführung einer Funktion nach einer Verzögerung.
setTimeout(function() {
console.log("Verzögerte Ausführung")
}, 2.5) // 2,5 Sekunden
$scriptImport(subScriptPath) — Subskript-Loader
Lädt und wertet eine andere JavaScript-Datei aus. Es werden nur lokale Dateipfade unterstützt; http(s)://- und file://-URLs werden abgelehnt. ($script ist für das Skript-Metadatenobjekt reserviert.)
$scriptImport("/path/to/helper.js")
// Daten zwischen Skripten mit $persistentStore übergeben
Details zu Skripttypen
Die drei HTTP-Skripttypen sehen eine unverschlüsselte HTTP-Anfrage nur, wenn sie Chute über seinen HTTP-Proxy erreicht, und eine HTTPS-Anfrage nur, wenn ihr Host entschlüsselt wird — tragen Sie den Host unter
hostnamein HTTPS-Entschlüsselung ein.
HTTP-Request-Skript
Wird ausgeführt, wenn Anfrage-Header empfangen werden. Kann URL, Header und Body ändern, bevor die Anfrage weitergeleitet wird.
[Script]
ModifyHeaders = type=http-request, script-path=modify.js, pattern=^https://api\.example\.com
HTTP-Response-Skript
Wird ausgeführt, wenn Antwort-Header empfangen werden. Kann Status, Header und Body ändern, bevor sie an den Client zurückgegeben werden.
[Script]
ModifyResponse = type=http-response, script-path=response.js, pattern=^https://api\.example\.com
Ein Skript, das den Body bekommt, läuft erst, wenn er vollständig angekommen ist. Schließt der Server bei HTTP/1.x die Verbindung vorher, ist ein Body ohne Content-Length und ohne Chunked-Kodierung vollständig — das Schließen ist sein Ende —, und das Skript läuft wie gewohnt. Ein Body mit Content-Length oder Chunked-Kodierung, den das Schließen abgeschnitten hat, wird dem Skript nicht übergeben: Er geht genau so an den Client, wie er ankam, und danach wird die Verbindung geschlossen. Eine Regel der Body-Umschreibung, die auf dieselbe Nachricht passt, wird zuerst angewendet, und das Skript sieht den umgeschriebenen Body.
HTTP-Request-Before-Send-Skript
Wird unmittelbar vor dem Senden der Anfrage an den Upstream ausgeführt. Wird der Body zurückgehalten — weil dieses Skript requires-body=true hat oder eine Body-Umschreibungsregel oder ein http-request-Skript den Body erhält —, läuft das Skript, sobald er vollständig angekommen ist. Eine Anfrage ohne Body oder mit einem Body über max-size wird nicht zurückgehalten, und ein Body, der während des Zurückhaltens über max-size wächst, geht so weiter, wie er kam: In beiden Fällen läuft das Skript beim Senden des Headers mit leerem Body, und ein Skript mit requires-body=true wird übersprungen. Nützlich zum Ändern von POST/PUT-Anfrage-Bodys. Es läuft zuletzt: nach jeder Regel der Body-Umschreibung und jedem http-request-Skript, das den Body erhält, und sieht den Body, den diese erzeugt haben. Ein vom Skript zurückgegebener Body ersetzt den eigenen Body der Anfrage, der verworfen wird, und Content-Length wird passend gesetzt.
[Script]
BeforeSend = type=http-request-before-send, script-path=before-send.js, pattern=^https://api\.example\.com, requires-body=true
Regel-Skript
Benutzerdefinierter Regelabgleich. Das Skript muss $done({matched: true}) oder $done({matched: false}) aufrufen.
[Rule]
SCRIPT,MyRuleScript,DIRECT
[Script]
MyRuleScript = type=rule, script-path=rule.js
DNS-Skript
Benutzerdefinierte DNS-Auflösung. Erhält $domain und gibt aufgelöste Adresse(n) zurück.
// dns.js
var domain = $domain
if (domain === "internal.example.com") {
$done({address: "10.0.0.1", ttl: 300})
} else {
$done({}) // Durchleitung zur normalen DNS-Auflösung
}
Ein [Host]-Eintrag <domain> = script:<name> leitet die Auflösung passender Domains an das genannte DNS-Skript — siehe Lokale DNS-Zuordnung.
Cron-Skript
Geplante Ausführung mit Cron-Ausdrücken. Das Mindestintervall beträgt 60 Sekunden.
[Script]
HalfHourlyTask = type=cron, script-path=task.js, cron-expression=*/30 * * * *
Der Ausdruck muss aus genau fünf Feldern bestehen, getrennt durch einzelne Leerzeichen. Alles andere — ein doppeltes Leerzeichen, ein Tabulator, vier Felder, sechs Felder — deaktiviert das Skript stillschweigend: Es wird nie eingeplant, und im Protokoll steht nichts.
Von den fünf Feldern wird nur das Minutenfeld berücksichtigt: */N läuft alle N Minuten. Jedes andere Minutenfeld wird alle 60 Sekunden ausgelöst, und das Skript muss die aktuelle Zeit selbst prüfen, um zu entscheiden, ob es handeln soll.
Event-Skript
Durch Systemereignisse ausgelöst. Drei Ereignisse werden ausgelöst:
| Ereignis | Wann es ausgelöst wird |
|---|---|
network-changed |
Das WLAN- oder Mobilfunknetz hat sich geändert |
engine-started |
Die Engine ist fertig gestartet — Richtlinien, Regeln und Listener sind aktiv |
profile-reloaded |
Ein Konfigurations-Neuladen ist abgeschlossen, bei den Skripten des neu geladenen Profils |
[Script]
NetChange = type=event, script-path=network-changed.js
Das Objekt $event ist verfügbar:
$event.name // "network-changed", "engine-started" oder "profile-reloaded"
Surge benennt das Ereignis mit event-name=, und Chute liest es ebenfalls: event-name=engine-started lässt das Skript nur bei diesem Ereignis laufen. Ein Skript, das kein Ereignis nennt, läuft bei allen dreien und liest $event.name, um zu erkennen, welches ausgelöst hat. Ein Skript, das an ein anderes Ereignis gebunden ist — Surge löst auch Ereignisse wie notification aus —, läuft in Chute nie; das Protokoll sagt es beim Laden der Konfiguration.
Praxisbeispiele
Mobile Geräte umleiten
Ein http-request-Skript, das mobile Benutzer basierend auf dem User-Agent umleitet:
[Script]
MobileRedirect = type=http-request, script-path=mobile-redirect.js, pattern=^https://example\.com
// mobile-redirect.js
var ua = $request.headers["User-Agent"] || ""
if (/Mobile|Android|iPhone/.test(ua)) {
$done({
response: {
status: 302,
headers: {"Location": "https://m.example.com" + $request.url.replace(/.*example\.com/, "")},
body: ""
}
})
} else {
$done({})
}
Inhalte in API-Antworten blockieren
Ein http-response-Skript, das Werbung und gesponserte Inhalte aus einer JSON-API-Antwort entfernt:
[Script]
RemoveAds = type=http-response, script-path=remove-ads.js, pattern=^https://api\.example\.com/feed, requires-body=true
// remove-ads.js
var body = JSON.parse($response.body)
if (body.ads) {
delete body.ads
}
if (body.recommendations) {
body.recommendations = body.recommendations.filter(function(r) {
return !r.sponsored
})
}
$done({body: JSON.stringify(body)})
Anfrage-Body vor dem Senden ändern
Ein http-request-before-send-Skript, das eine POST-Nutzlast bereinigt:
[Script]
SanitizePayload = type=http-request-before-send, script-path=sanitize.js, pattern=^https://api\.example\.com/submit, requires-body=true
// sanitize.js
var body = JSON.parse($request.body)
body.clientSecret = "[REDACTED]"
body.timestamp = Math.floor(Date.now() / 1000)
$done({body: JSON.stringify(body)})
Benutzerdefinierte Regel: Zeitbasiertes Routing
Ein rule-Skript, das je nach Tageszeit einen anderen Proxy auswählt:
[Rule]
SCRIPT,TimeBasedRule,ProxyA
[Script]
TimeBasedRule = type=rule, script-path=time-rule.js
// time-rule.js
var hour = new Date().getHours()
if (hour >= 9 && hour < 18) {
$done({matched: false}) // Während der Arbeitszeit zur nächsten Regel durchfallen
} else {
$done({matched: true}) // ProxyA außerhalb der Arbeitszeit verwenden
}
Benutzerdefiniertes DNS für interne Domains
Ein dns-Skript, das interne Hostnamen zu lokalen IPs auflöst:
[Script]
InternalDNS = type=dns, script-path=internal-dns.js
// internal-dns.js
var internalHosts = {
"gitlab.local": "10.0.0.10",
"registry.local": "10.0.0.11",
"monitor.local": "10.0.0.12"
}
if (internalHosts[$domain]) {
$done({address: internalHosts[$domain], ttl: 3600})
} else {
$done({}) // Durchleitung zur normalen DNS-Auflösung
}
Richtlinie bei Netzwerkwechsel automatisch umschalten
Ein event-Skript, das nach einem Netzwerkwechsel die Konnektivität prüft und die Richtliniengruppe entsprechend umschaltet:
[Script]
NetSwitch = type=event, script-path=network-switch.js, timeout=15
// network-switch.js
$httpClient.head("https://www.google.com/generate_204", {timeout: 5},
function(error, response, data) {
if (error) {
// Probe im neuen Netzwerk fehlgeschlagen — zur Backup-Gruppe wechseln
$klne.selectPolicy("MainGroup", "BackupProxy")
console.log("Probe failed after " + $event.name)
} else {
$klne.selectPolicy("MainGroup", "MainProxy")
}
$done({})
})
Hinweis:
$networkist für jedes Skript sichtbar, aberwifi.ssidwird nur unter iOS und Android gefüllt (siehe den Hinweis unter $network). Unter macOS und tvOS bleibt der Name leer; ein Event- oder Cron-Skript, das überall funktionieren soll, prüft daher wie oben mit$httpClient, statt die SSID zu lesen.
Periodische Zustandsprüfung
Ein cron-Skript, das alle 30 Minuten den Zustand des Proxys prüft:
[Script]
HealthCheck = type=cron, script-path=health-check.js, timeout=15, cron-expression=*/30 * * * *
// health-check.js
$httpClient.head("https://www.google.com/generate_204", {timeout: 10},
function(error, response, data) {
if (error || response.status !== 204) {
console.error("Zustandsprüfung fehlgeschlagen: " + (error || "Status " + response.status))
$notification.post("Chute-Alarm", "Zustandsprüfung", "Google nicht erreichbar")
} else {
console.log("Zustandsprüfung OK")
}
$done({})
}
)
Rufen Sie
$done({})innerhalb des Callbacks auf — der Abschluss des Skripts bricht alle ausstehenden$httpClient-Anfragen ab, sodass ein synchrones$done()am Skriptende die Zustandsprüfung abbrechen würde, bevor die Antwort eintrifft. Aus demselben Grund muss das eigenetimeouteiner Anfrage kürzer sein als das des Skripts: Läuft dastimeoutdes Skripts (standardmäßig 5 Sekunden) zuerst ab, wird die Anfrage abgebrochen, und ihr Callback läuft nie. Beide Beispiele oben setzentimeout=15in der[Script]-Zeile.
API-Antworten mit externen Daten anreichern
Ein http-response-Skript, das Benutzerdaten durch Aufruf einer sekundären API anreichert:
[Script]
EnrichUsers = type=http-response, script-path=enrich.js, pattern=^https://api\.example\.com/users, requires-body=true, timeout=20
// enrich.js — at most 8 requests in flight, the per-script limit
var users = JSON.parse($response.body)
var next = 0
var pending = 0
function launch() {
while (pending < 8 && next < users.length) {
fetchAvatar(next++)
}
if (pending === 0) {
$done({body: JSON.stringify(users)})
}
}
function fetchAvatar(index) {
pending++
$httpClient.get("https://internal-api.example.com/avatar/" + users[index].id,
function(error, resp, data) {
if (!error && resp.status === 200) {
users[index].avatar = JSON.parse(data).url
}
pending--
launch()
}
)
}
launch()
Ausführungsmodell
- Alle Skripte laufen in einer dedizierten seriellen Warteschlange für Thread-Sicherheit.
- Jede Skriptausführung hat ein Timeout je Skript, gezählt ab dem Beginn der Ausführung: Das Laden des Quelltexts (ein Remote-Skript wird innerhalb dieser Zeit heruntergeladen), das Ausführen des Codes und das Warten auf Timer und Anfragen teilen es sich. Wenn
$done()nicht innerhalb des Timeouts aufgerufen wird, wird das Skript als Durchleitung behandelt. Ein Skript, das ohne$done()zurückkehrt, während weder einsetTimeout-Timer noch eine$httpClient-Anfrage aussteht, wird sofort als Durchleitung behandelt. - Ein Pool von 3 vorgewärmten Kontexten wird vorgehalten; ein Kontext wird nach jeder Ausführung verworfen und durch einen frischen ersetzt, sodass globale Variablen niemals zwischen Läufen durchsickern.
- Chute notiert beim Start der Skript-Engine den Speicherverbrauch des Prozesses. Ist er um mehr als 10 MB auf iOS und tvOS bzw. 512 MB auf macOS gewachsen, wird jedes Skript übersprungen und seine Nachricht durchgeleitet; auf Android ist das Maß der belegte Java-Heap, mit einem Spielraum von 512 MB. Der Spielraum gilt für den gesamten Prozess, nicht für ein einzelnes Skript, und der Ausgangswert wird nicht neu erfasst, solange Chute läuft.
- Auf iOS, tvOS und Android dürfen höchstens 16 Skriptausführungen gleichzeitig laufen (64 auf macOS), mit zusammen höchstens 4 MB Skriptquelltext (8 MB auf macOS). Eine Ausführung, die eine der beiden Grenzen überschreitet, wird übersprungen und ihre Nachricht durchgeleitet; das Protokoll meldet
execution admission is full. - Der Quelltext eines Skripts darf höchstens 1 MB (macOS) bzw. 512 KB (iOS, tvOS und Android) groß sein. Wie viele Skripte eine Konfiguration deklarieren darf, ist nicht begrenzt; begrenzt ist, wie viele Skriptquellen gleichzeitig geladen werden dürfen — 32 auf macOS, 8 auf iOS, tvOS und Android. Oberhalb dieser Grenze meldet das Protokoll
source load queue is full, und diese eine Ausführung läuft ohne Quelltext, was einer Durchleitung entspricht. - Remote-Skripte (HTTP/HTTPS-Pfade) werden bei jeder Ausführung abgerufen. Ein positives
script-update-intervaloder genau-1lässt Chute die URL zusätzlich alle 10 Minuten mit einer bedingten HEAD-Anfrage abfragen. Der Wert ist nur ein Schalter, keine Periode: Abgefragt wird alle 10 Minuten, was auch immer die Zahl sagt.0(der Standard) und jeder andere negative Wert lassen die Abfrage ausgeschaltet.
Modulskript-Integration
Skripte können auch in Modul-Dateien (.sgmodule) unter dem Abschnitt [Script] definiert werden.
Diese Seite ist eine Übersetzung der englischen Version. Bei Abweichungen ist die englische Version maßgeblich.