Scriptage JavaScript

Chute prend en charge le scriptage JavaScript pour la modification avancée des requêtes/réponses, la correspondance de règles personnalisée, la résolution DNS et les tâches planifiées. Les scripts s'exécutent avec JavaScriptCore sur les plateformes Apple et avec QuickJS sur Android, et suivent l'API de script compatible Surge.

Les scripts sont définis dans la section [Script] du fichier de configuration.

Configuration

[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 * * * *

Paramètres de script

Paramètre Requis Par défaut Description
type Non http-request Type de déclenchement du script (voir ci-dessous)
script-path Oui — Chemin de fichier local ou URL HTTP(S) du script JS
pattern Non (tout correspond) Motif regex d'URL filtrant le déclenchement du script. Il est recherché n'importe où dans l'URL complète de la requête — https://… une fois la requête déchiffrée, http://… pour le HTTP en clair — et jamais dans le chemin seul, si bien qu'un motif comme ^/api ne correspond pas
requires-body Non détection auto Force le script à recevoir le corps complet de requête/réponse
max-size Non 131072 (128 Ko) Taille de corps maximale, en octets, pour les scripts accédant au corps. Si le corps collecté dépasse cette taille, le script est ignoré pour cette requête (le corps n'est pas tronqué). En HTTP/1.x — HTTP en clair et HTTP/1.1 déchiffré — Chute ne met en mémoire tampon que 131072 octets au plus pour les scripts, quelle que soit cette valeur : elle ne peut donc qu'y abaisser la limite ; un message HTTP/2 déchiffré est transmis en entier et comparé à max-size seul
timeout Non 5,0 secondes Délai par exécution, compté depuis le début : le chargement de la source (son téléchargement, pour un script distant), l'exécution du code et l'attente des minuteurs et des requêtes se partagent tous ce délai. Les valeurs supérieures à 30 secondes sont ramenées à 30
argument Non — Argument chaîne personnalisé, disponible en JS via $argument
full-header-mode Non false Avec true, $request.headers et $response.headers sont des tableaux de {field, value}, un par ligne d'en-tête (le format de Surge), au lieu d'objets. S'applique aux scripts http-request, http-request-before-send et http-response ; $done() accepte les deux formes dans tous les cas. Voir En-têtes sur plusieurs lignes
debug Non false Réservé ; sans effet actuellement
cron-expression Non — Expression de planification cron (type cron uniquement) ; cronexp= et le cronexpr= de Shadowrocket sont acceptés comme la même clé
event-name Non (tous les événements) L'événement qu'écoute un script event — network-changed, engine-started ou profile-reloaded ; sans nom, le script s'exécute pour les trois (voir Script d'événement)
wake-system Non false Réservé ; analysé mais sans effet actuellement
enable Non true Active ou désactive ce script
script-update-interval Non 0 Interrogation de mise à jour des scripts distants (voir Modèle d'exécution)
binary-body-mode Non — Accepté pour la compatibilité avec Surge (aussi écrit binary-mode) et conservé à l'enregistrement du profil, mais il ne change rien : les octets bruts sont toujours dans .bodyBytes
engine Non — Accepté et conservé, mais ignoré : chaque script s'exécute sur le moteur propre à la plateforme

Types de scripts

Chaîne de type Énumération Description
http-request Requête HTTP Intercepte et modifie les requêtes HTTP avant l'amont
http-response Réponse HTTP Intercepte et modifie les réponses HTTP avant le client
http-request-before-send Requête HTTP avant envoi Modifie la requête juste avant son envoi vers l'amont
rule Règle Logique de correspondance de règle personnalisée
dns DNS Résolution DNS personnalisée
cron Cron Scripts planifiés/temporisés
event Événement Gestionnaires d'événements système (par ex. network-changed)
generic Générique Accepté pour la compatibilité avec Surge ; il n'est rattaché à aucune requête, règle, résolution DNS ni aucun événement, donc Chute ne l'exécute jamais de lui-même. Exécutez-le avec Exécuter dans la liste des scripts de Chute iOS ou de Chute Android, ou avec POST /api/scripts/run de l'API de contrôle HTTP

Un type= inconnu est signalé dans le journal et traité comme http-request.

Détection automatique du corps : pour les scripts http-request et http-response, une source qui contient $request.body ou $response.body reçoit automatiquement le corps, dans la limite de max-size. Utilisez requires-body=true pour forcer ce comportement. Un script qui ne lit que .bodyBytes ou .rawBody n'est pas détecté, et les scripts http-request-before-send ne le sont jamais : ceux-là ont besoin de requires-body=true.


Référence de l'API JavaScript

Les scripts s'exécutent dans un environnement JavaScript isolé — JavaScriptCore sur les plateformes Apple, QuickJS sur Android — où les objets globaux suivants sont disponibles.

$request (lecture seule)

Disponible dans : http-request, http-response, http-request-before-send, rule, dns

Propriété Type Description
.url String URL complète de la requête ; le port y est écrit, sauf s'il s'agit du port par défaut du schéma, et un hôte IPv6 est entre crochets. Une cible de requête qui est déjà une URL absolue est reprise telle quelle, une cible qui ne commence pas par / en reçoit un, et sans hôte connu la valeur n'est que le chemin. Dans un script rule, c'est l'hôte de destination ; dans un script dns, le domaine interrogé
.method String Méthode HTTP (GET, POST, etc.) ou QUERY pour le DNS
.headers Object ou Array En-têtes de requête, chaque nom avec sa valeur ; les valeurs d'un en-tête sur plusieurs lignes sont réunies en une seule chaîne (voir En-têtes sur plusieurs lignes). Un tableau de {field, value} avec full-header-mode=true. Les noms se retrouvent quelle que soit leur casse (voir Noms d'en-tête)
.body String ou null Corps de la requête (décodé en UTF-8)
.bodyBytes Bytes ou null Octets bruts du corps de la requête (voir la remarque ci-dessous)
.hostname String Nom d'hôte cible
.destPort Number Port de destination
.processPath String Chemin du processus émetteur (macOS ; sur Android, le chemin de l'APK de l'app, pour les connexions TCP que capture le VPN)
.userAgent String Valeur de l'en-tête User-Agent
.sourceIP String Adresse IP source
.listenPort Number Port d'écoute du proxy
.requestId String Identifiant unique de la requête
.dnsResult String Adresse IP résolue
.srcPort Number Port source
.protocol String Protocole détecté : http, https, tcp, dns

$response (lecture seule)

Disponible dans : http-response

Propriété Type Description
.status Number Code de statut HTTP
.headers Object ou Array En-têtes de réponse, chaque nom avec sa valeur ; les valeurs d'un en-tête sur plusieurs lignes sont réunies en une seule chaîne (voir En-têtes sur plusieurs lignes). Un tableau de {field, value} avec full-header-mode=true. Les noms se retrouvent quelle que soit leur casse (voir Noms d'en-tête)
.body String ou null Corps de la réponse (décodé en UTF-8)
.bodyBytes Bytes ou null Octets bruts du corps de la réponse (voir la remarque ci-dessous)
.rawBody Bytes ou null Alias de .bodyBytes

.body, .bodyBytes et .rawBody portent le même corps : avant que le script ne le voie, le découpage chunked est retiré et un Content-Encoding gzip ou deflate est défait. .bodyBytes et .rawBody contiennent un objet d'octets. Passez-le à un appel qui prend des octets — $utils.ungzip(), dont le résultat est lui aussi un objet d'octets, ou body dans $done(), qui le prend comme corps brut. La nature de cet objet dépend du moteur : une enveloppe opaque sans .length ni indexation sur les plateformes Apple, un Uint8Array sur Android ; un script destiné à toutes les plateformes ne devrait donc compter ni sur .length ni sur l'indexation. Pour en lire le contenu, utilisez .body, qui est ces mêmes octets décodés en UTF-8.

$done(value) — gestionnaire d'achèvement

Doit être appelé exactement une fois à la fin du script pour signaler son achèvement. Un script qui rend la main sans l'avoir appelé est achevé immédiatement en passe-plat, sauf si une minuteur setTimeout ou une requête $httpClient est encore en attente. timeout= court dès le début de l'exécution : le chargement de la source, l'exécution du code et cette attente sont tous décomptés de ce délai.

$done({})                        // Passe-plat — aucune modification
$done()                          // Interrompt la connexion
$done({matched: true})           // Résultat de correspondance (scripts de règle uniquement)
$done({address: "1.2.3.4"})      // Résultat DNS (scripts dns uniquement)

Dans un script http-request, http-request-before-send ou http-response, $done() sans argument — ou avec une valeur qui n'est pas un objet — interrompt la connexion : Chute n'envoie rien à la place de la requête ou de la réponse, et une requête interrompue n'atteint jamais le serveur. Ce qui est déjà en route vers le client est écrit d'abord, puis la connexion se ferme aussitôt. En HTTP/2, seul le flux de cette requête est réinitialisé (RST_STREAM avec le code d'erreur CANCEL) et rien n'est envoyé à sa place ; la connexion et les autres requêtes qu'elle porte continuent.

Les en-têtes et l'url renvoyés par un script sont vérifiés avant usage. Un en-tête est abandonné, avec un avertissement dans le journal, quand :

  • son nom ou sa valeur contient un saut de ligne (CR ou LF) ou un caractère NUL : [JS] Dropped header <name>: a header name or value cannot contain a line break ;
  • son nom n'est pas un nom de champ HTTP valide (RFC 9110 §5.1). Un nom valide se compose d'une ou plusieurs lettres, chiffres ou caractères parmi !#$%&'*+-.^_`|~ ; un nom vide, ou qui contient une espace, un deux-points ou une lettre non ASCII, est donc abandonné : [JS] Dropped header <name>: not a valid header name.

Le reste du résultat s'applique. Les mêmes vérifications portent sur les en-têtes donnés sous forme de tableau et sur les en-têtes d'une response renvoyée par un script de requête. Une url qui contient un saut de ligne, un NUL ou une espace est ignorée, avec un avertissement également. Le chemin et la requête d'une url sont envoyés tels qu'écrits : les échappements comme %20 restent échappés.

Valeurs de retour d'un script de requête HTTP :

$done({
    url: "https://new.example.com/path",     // Réécrit l'URL
    headers: {"X-Custom": "value"},           // Modifie les en-têtes
    body: "new request body",                 // Modifie le corps
    response: {                               // Renvoie une réponse synthétique (ignore l'amont)
        status: 200,
        headers: {"Content-Type": "text/html"},
        body: "<html>Blocked</html>"
    }
})

Lorsque response est fourni, la requête est court-circuitée : Chute renvoie la réponse synthétique directement au client, et la requête n'est pas envoyée au serveur amont. C'est vrai partout où Chute traite la requête, HTTP/2 compris, que le script s'exécute sur l'en-tête, sur le corps entier ou en tant que http-request-before-send. En HTTP/1.x, la ligne de statut porte la phrase de raison standard du code (HTTP/1.1 404 Not Found) et la connexion se ferme une fois la réponse envoyée ; en HTTP/2, seul le flux de cette requête reçoit la réponse. status est un nombre de 200 à 599, ou une chaîne qui contient un tel nombre entier une fois les espaces autour retirées ("404") ; toute autre valeur, ou aucune, donne 200. Content-Length est calculé à partir de body, quoi que dise headers. C'est utile pour bloquer, simuler des API ou renvoyer du contenu en cache.

Les réponses que Chute génère lui-même — la Réponse simulée, la response d'un script, ainsi que les réponses et la page d'erreur de la Réécriture d'URL et des politiques REJECT — suivent HTTP : la réponse à une requête HEAD se limite à l'en-tête, avec le Content-Length qu'obtiendrait un GET, et une réponse de statut 204, 205 ou 304 ne porte ni corps ni Content-Length.

Valeurs de retour d'un script de réponse HTTP :

$done({
    status: 200,                              // Modifie le code de statut
    headers: {"X-Custom": "value"},           // Modifie les en-têtes de réponse
    body: "new response body",                // Modifie le corps de la réponse
    url: "https://other.example.com"          // Redirection (302 par défaut)
})

body remplace le corps de la réponse et rien d'autre : la ligne de statut et les en-têtes restent ceux du serveur, avec le status et les headers renvoyés fusionnés, et le corps d'origine du serveur est abandonné. Si le script ne s'est exécuté que sur l'en-tête, les règles de réécriture de corps et les scripts http-response qui reçoivent le corps s'exécutent ensuite comme d'habitude, sur le nouveau corps. Une réponse à HEAD, ou dont le statut est 1xx, 204, 205 ou 304, n'a pas de corps : le body renvoyé y est ignoré, tandis que status et headers s'appliquent toujours.

status est un nombre de 200 à 599, ou une chaîne qui contient un tel nombre entier une fois les espaces autour retirées ("301"). Toute autre valeur, true et false compris, est ignorée, et la réponse garde son statut. Un status qui change le code donne aussi à la ligne de statut la phrase de raison standard du nouveau code. Un status de 204, 205 ou 304 envoie la réponse sans corps — ni celui du serveur, ni un body renvoyé — et sans en-tête de longueur ; en HTTP/1.x, la connexion se ferme une fois l'en-tête envoyé.

Lorsque url est fourni, Chute répond par une redirection vers cette URL au lieu de la réponse d'origine. Son statut est le status renvoyé — 301, 307 ou 308, par exemple — ou 302 s'il n'y en a pas ou s'il est ignoré ; les headers renvoyés sont fusionnés comme d'habitude, et Location vaut url. Sans body, la redirection n'a pas de corps, et le corps du serveur n'est jamais envoyé ; en HTTP/2, les trailers du serveur ne le sont pas non plus. url doit être renvoyé conjointement à au moins l'un de status / headers / body ; un résultat ne contenant que url est traité comme un passe-plat.

Valeurs de retour d'un script DNS :

$done({address: "1.2.3.4"})                  // IP unique
$done({addresses: ["1.2.3.4", "5.6.7.8"]})   // Plusieurs IP
$done({address: "10.0.0.1", ttl: 300})       // Avec TTL personnalisé (secondes, 60 par défaut)
$done({server: "1.1.1.1"})                   // Résoudre via ce serveur
$done({servers: ["tls://dns.google", "8.8.8.8#disable-ipv6"]})

server / servers ne répondent pas : ils choisissent le résolveur. Chaque entrée est une entrée de serveur DNS ordinaire — une adresse, tls://, https://, avec les options # habituelles — et elles sont interrogées ensemble, la première réponse l'emporte. Si aucune entrée n'est utilisable, la résolution échoue au lieu de revenir au pool configuré.

En-têtes sur plusieurs lignes

Un en-tête peut occuper plusieurs lignes : une réponse qui pose deux cookies porte deux lignes Set-Cookie. Les scripts http-request, http-request-before-send et http-response peuvent lire chaque ligne et en écrire plusieurs.

Lecture

Par défaut, $request.headers et $response.headers sont des objets qui associent à chaque nom d'en-tête une seule chaîne. Quand un en-tête a plusieurs lignes, leurs valeurs sont réunies dans l'ordre : celles de Cookie avec "; ", celles de tout autre en-tête (Set-Cookie compris) avec ", ".

$response.headers["Set-Cookie"]  // "a=1; Path=/, b=2; Path=/"
$request.headers["Cookie"]       // "a=1; b=2"

La date Expires d'un cookie contient une virgule : une chaîne Set-Cookie réunie ne peut donc pas être redécoupée de façon fiable en cookies. Pour lire chaque ligne séparément, ajoutez full-header-mode=true à la ligne du script. $request.headers et $response.headers sont alors des tableaux contenant un objet {field, value} par ligne, le format qu'utilise Surge. Les lignes de même nom gardent leur ordre.

// 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=/"]

Il en va de même en HTTP/2 : quand un client répartit son cookie sur plusieurs champs cookie, le script les lit comme un seul Cookie réuni avec "; ", et chaque set-cookie est une ligne à part.

Écriture

headers dans $done() ne change que les en-têtes qu'il nomme ; tous les autres restent tels quels. Les mêmes règles s'appliquent à headers à l'intérieur d'une response par laquelle un script de requête répond.

Valeur Effet
une chaîne L'en-tête reçoit exactement une ligne avec cette valeur ; toutes ses lignes existantes sont remplacées
un tableau de chaînes Une ligne par valeur, dans l'ordre, à la place des lignes de l'en-tête. {"Set-Cookie": ["a=1", "b=2"]} envoie deux lignes Set-Cookie
[] L'en-tête est supprimé
toute autre chose (un nombre, null, un objet, un tableau contenant autre chose qu'une chaîne) Ignorée : l'en-tête reste tel quel

Une chaîne réunie qu'un script renvoie exactement telle qu'il l'a lue conserve les lignes d'origine de l'en-tête. Ainsi const h = $response.headers; h["X-A"] = "1"; $done({headers: h}) ne change que X-A, et deux lignes Set-Cookie partent toujours en deux lignes. Toute autre chaîne remplace toutes les lignes de l'en-tête par une seule. Pour ajouter un cookie, écrivez la liste complète sous forme de tableau : lisez les lignes existantes en mode full-header, puis renvoyez {"Set-Cookie": [...existing, "c=3"]}.

Une requête porte une seule ligne Cookie : un tableau donné pour Cookie dans les headers d'un script http-request ou http-request-before-send est réuni avec "; ", si bien que {"Cookie": ["a=1", "b=2"]} envoie Cookie: a=1; b=2.

$done() accepte aussi headers sous forme de tableau d'objets {field, value}, avec ou sans full-header-mode :

$done({headers: [{field: "Set-Cookie", value: "a=1"},
                 {field: "Set-Cookie", value: "b=2"},
                 {field: "X-A", value: "1"}]})

Les entrées sont regroupées par nom, sans tenir compte de la casse, et chaque groupe remplace les lignes de cet en-tête dans l'ordre donné. Les en-têtes que le tableau ne nomme pas restent tels quels : omettre un en-tête ne le supprime donc pas ; pour en supprimer un, utilisez la forme objet avec []. Une entrée sans field et value de type chaîne est ignorée.

  • Les noms d'en-tête ne tiennent pas compte de la casse : set-cookie et Set-Cookie sont le même en-tête, donnez donc chaque nom une seule fois. Un nom qui n'est pas un nom d'en-tête valide est abandonné, comme décrit sous $done.
  • Une valeur qui contient un saut de ligne ou un NUL est abandonnée, comme décrit sous $done. Dans un tableau, seule cette valeur est abandonnée ; si toutes le sont, l'en-tête reste tel quel.
  • En HTTP/2, chaque ligne devient un champ à part, au nom en minuscules. Les en-têtes propres à une seule connexion, comme Connection et Keep-Alive, ne sont pas envoyés.

Noms d'en-tête dans $request.headers et $response.headers

$request.headers et $response.headers trouvent un en-tête quelle que soit la casse dans laquelle son nom est écrit : $response.headers['ETag'], $response.headers['etag'] et $response.headers['Etag'] lisent tous le même en-tête. Cela vaut aussi pour in ('etag' in $response.headers), pour l'affectation (headers['content-type'] = 'text/html' modifie le Content-Type déjà présent dans l'objet au lieu de lui ajouter un second nom) et pour delete.

Quand vous énumérez les noms (Object.keys(), for…in, JSON.stringify()), chaque en-tête apparaît une fois, orthographié comme Chute le stocke — chaque mot avec une majuscule, quelle que soit la façon dont le client ou le serveur l'a écrit : Content-Type, Etag, X-Api-Key, Www-Authenticate.

Seul l'objet que vous lisez depuis $request.headers ou $response.headers fait correspondre les noms de cette façon. Une copie que vous faites vous-même, par exemple avec Object.assign({}, $request.headers) ou {...$response.headers}, est un objet ordinaire ; dans une copie, utilisez l'orthographe montrée ci-dessus. Avec full-header-mode=true, headers est un tableau d'objets {field, value}, dont field suit la même orthographe, et cette recherche ne s'applique pas ; comparez plutôt field.toLowerCase().

$httpClient — client HTTP asynchrone

Effectue des requêtes HTTP depuis les scripts. Toutes les requêtes sont annulées à l'appel de $done() ou à l'expiration du délai.

$httpClient.get(url, function(error, response, data) {
    if (error) {
        console.log("Request failed: " + error)
    } else {
        console.log("Status: " + response.status)
        console.log("Response: " + 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)

Un objet d'options peut aussi porter policy, le nom d'une politique définie par le profil : la requête est alors émise via cette politique plutôt que par la route par défaut. Un nom que le profil ne définit pas est consigné comme avertissement, et la requête prend la route par défaut. La forme en ligne policy-descriptor de Surge n'est pas prise en charge : elle est consignée comme avertissement, et la requête prend elle aussi la route par défaut — définissez la politique dans le profil et transmettez son nom.

Signature du rappel : callback(error, response, data)

  • error : chaîne d'erreur ou null
  • response : {status: Number, headers: Object} ou null. headers trouve un en-tête quelle que soit la casse de son nom et énumère les noms comme $request.headers — chaque mot avec une majuscule, comme Content-Type, Etag, X-Api-Key —, avec ou sans policy
  • data : le corps de la réponse décodé en chaîne UTF-8 ; un corps qui n'est pas de l'UTF-8 valide arrive sous la forme d'un objet d'octets semblable à .bodyBytes, et non comme null. null uniquement lorsqu'il n'y a pas de corps

$httpClient est disponible dans les scripts http-request, http-response, http-request-before-send, cron et event. Il n'est pas disponible dans les scripts rule et dns.

Un script peut avoir au plus 8 requêtes en cours, et l'ensemble des scripts 16. Une requête qui dépasse l'une ou l'autre limite n'est pas envoyée et son rappel ne s'exécute jamais ; le journal le signale une fois par exécution. Un corps de réponse de plus de 4 Mo fait échouer la requête.

$persistentStore — stockage clé-valeur

Stockage clé-valeur persistant qui survit aux redémarrages du script et du processus ; tous les scripts lisent et écrivent les mêmes clés. Sur Android, il est conservé dans le stockage privé de Chute et exclu des sauvegardes de l'appareil.

$persistentStore.write(data, key)   // Enregistre une valeur
$persistentStore.read(key)          // Récupère une valeur
$persistentStore.remove(key)        // Supprime une valeur

$notification — notifications locales

Publie des notifications système locales. Chute Apple TV n'en affiche aucune.

$notification.post("Title", "Subtitle", "Notification body text")

Un quatrième argument facultatif transmet les options de Surge : url (le lien à ouvrir quand on touche la notification), action (open-url est implicite avec url, et c'est la seule action que les apps prennent en charge — les autres, comme clipboard de Surge, sont transmises mais rien ne les traite) et auto-dismiss (secondes). Elles sont jointes à la notification ; les autres clés sont ignorées avec un avertissement. url est pris en charge par Chute iOS, Chute Mac et Chute Android, qui l'ouvrent lorsqu'on clique sur la notification. auto-dismiss n'est pris en charge que par Chute Android, qui retire la notification au bout de ce nombre de secondes ; les versions Apple la laissent en place.

$notification.post("Title", "", "Tap to open", {url: "https://example.com", "auto-dismiss": 5})

Les notifications des scripts — comme celles des règles avec notification-text — suivent l'interrupteur de notifications de l'app : Autoriser les notifications sur Chute iOS, Afficher les notifications d'événements signalés sur Chute Mac, l'autorisation de notification que le système accorde à Chute sur Chute Android. Désactivez-le et rien ne s'affiche. Chute Android les publie dans un canal de notification qui leur est propre, Notifications des scripts, que l'on peut désactiver séparément dans les réglages de notification du système. Voir Rapports de notification.

$network — informations réseau

Informations d'état réseau en lecture seule.

$network.dns   // Tableau des IP de serveurs DNS
$network.wifi          // {ssid: "WiFiName", bssid: null}
$network.v4            // {primaryAddress, primaryRouter}
$network.v6            // {primaryAddress, primaryRouter}
$network.primaryRouter // Passerelle IPv4 par défaut, ou null
$network.cellularData  // {radio: "LTE" | "5G" | ..., carrier: null}

ssid et bssid sont renseignés sur iOS, où le tunnel peut lire l'identité du Wi-Fi, et sur Android lorsque Chute détient la permission qu'Android exige pour lire le nom du réseau (voir Premiers pas sur Android) ; sur macOS et tvOS, ssid est vide et bssid vaut null. primaryAddress ignore l'adresse propre du tunnel et les adresses lien-local : c'est donc l'adresse avec laquelle l'appareil atteint le réseau. carrier vaut toujours null — iOS 16 a supprimé le nom de l'opérateur.

$environment — informations d'exécution

$environment.system     // "iOS", "macOS" ou "Android"
$environment.appVersion   // Chaîne de version du SDK KLNEKit
$environment.surgeVersion // Version du moteur lui-même, sous le nom que lisent les scripts Surge
$environment.language     // Langue préférée, en étiquette BCP 47, par ex. "en-US"
$environment.deviceModel  // "iPhone", "Mac" ou "AppleTV" ; sur Android, le modèle de l'appareil, par ex. "Pixel 8"

$utils — utilitaires

$utils.geoip("1.2.3.4")   // Code pays (par ex. "US")
$utils.ipasn("1.2.3.4")   // Numéro d'ASN (par ex. "13335")
$utils.ipaso("1.2.3.4")   // Organisation de l'ASN (par ex. "CLOUDFLARENET")
$utils.ungzip(data)       // Décompresse des données gzip

$klne — API de contrôle du proxy

Pilote le proxy à l'exécution depuis les scripts. $klne est disponible dans tous les types de scripts.

$klne.getPolicyGroups()               // Récupère tous les groupes de politiques
$klne.selectGroupDetails()            // Les mêmes groupes, dans la forme de Surge
$klne.selectPolicy("Group", "Proxy")  // Change la politique d'un groupe
$klne.getActiveConnections()          // Liste les connexions actives
$klne.closeConnection("id")           // Ferme la connexion portant l'id renvoyé par getActiveConnections()
$klne.flushDNS()                      // Purge le cache DNS
$klne.startURLTest("Group")           // Déclenche un test d'URL pour un groupe
$klne.reloadConfiguration()           // Recharge toute la configuration
$klne.setOutboundMode("rule")         // Définit le mode : "global"/"proxy", "direct", "rule"
$klne.setHTTPCaptureEnabled(true)     // Active/désactive le MitM
$klne.setRewriteEnabled(true)         // Active/désactive la famille de réécriture

getPolicyGroups() renvoie les groupes de politiques sélectionnables :

{
  count: 1,                    // Number — nombre de groupes
  policyGroups: [{
    name: "MainGroup",         // String — nom du groupe
    type: 0,                   // Number — 0 select, 1 url-test, 2 fallback, 3 ssid, 5 load-balance
    policyNames: ["A", "B"],   // Array of String — les membres réels du groupe, nœuds issus d'abonnements compris
    selectedIndex: 0,          // Number — index de la politique active dans policyNames (absent si non résolue)
    selectedPolicy: "A"        // String — nom de la politique active (absent si non résolue)
  }]
}

selectGroupDetails() renvoie les mêmes groupes dans la forme de Surge — policyGroups associe chaque nom de groupe aux noms de ses membres, decisions associe chaque nom de groupe au membre actuellement sélectionné :

{
  policyGroups: {MainGroup: ["A", "B"]},
  decisions: {MainGroup: "A"}
}

Un groupe dont la sélection n'est pas résolue est absent de decisions.

getActiveConnections() renvoie un tableau décrivant les connexions en cours :

[{
  id: 1042,             // Number — le numéro de connexion, celui que closeConnection accepte
  host: "example.com",   // String — hôte de destination (absent si inconnu)
  port: 443              // Number — port de destination
}]

Les autres méthodes prennent des arguments simples et ne renvoient rien :

  • selectPolicy(group, policy) — fait de policy (un nom issu des policyNames du groupe) la politique active de group. Un groupe ou un nom de politique inconnu est ignoré avec un avertissement dans le journal.
  • closeConnection(id) — ferme la connexion portant cet id, le numéro que renvoie getActiveConnections(). Un id qui n'est pas ouvert est ignoré avec un avertissement.
  • flushDNS() — purge le cache DNS.
  • startURLTest(group) — démarre un test de latence asynchrone pour un groupe url-test, fallback ou load-balance ; les autres types de groupes et les noms inconnus sont ignorés avec un avertissement.
  • reloadConfiguration() — récupère immédiatement la source #!MANAGED-CONFIG du profil, sans attendre l'intervalle de mise à jour, et l'applique si elle diffère. Réappliquer la configuration que le moteur détient déjà ne changerait rien : un profil sans source gérée se contente donc de consigner un avertissement.
  • setOutboundMode(mode) — "global" et "proxy" font passer tout le trafic par le proxy, "direct" l'envoie directement, et toute autre valeur sélectionne le mode règles.
  • setHTTPCaptureEnabled(enabled) — active ou désactive le déchiffrement HTTPS (MitM) à l'exécution ; prend un booléen.
  • setRewriteEnabled(enabled) — active ou désactive à l'exécution toute la famille de réécriture — Réécriture d'URL, Réécriture d'en-tête, Réécriture de corps et Réponse simulée ; prend un booléen.

$surge est disponible pour les scripts Surge, avec les appels dont le sens est identique : $surge.setSelectGroupPolicy(group, policy) (identique à selectPolicy), $surge.selectGroupDetails() (identique à selectGroupDetails), $surge.setOutboundMode(mode), $surge.setHTTPCaptureEnabled(enabled), $surge.setRewriteEnabled(enabled) (toute la famille de réécriture : Réécriture d'URL, Réécriture d'en-tête, Réécriture de corps et Réponse simulée) et $surge.retestGroup(name). Le reste de $surge n'a pas d'équivalent ici et se lit undefined, ce qu'un script peut tester.

$httpAPI — passerelle vers l'API de contrôle

Appelle l'API de contrôle HTTP du moteur depuis un script. $httpAPI est disponible dans tous les types de scripts.

$httpAPI("GET", "/api/status", null, function(result) {
    console.log(result.statusCode)   // Number — le statut HTTP
    console.log(result.body.data)    // Object — le corps JSON analysé
})

$httpAPI("/api/status")                  // Un seul argument : un GET de ce chemin
$httpAPI("DELETE", "/api/dns/cache")     // Méthode et chemin
var result = $httpAPI("GET", "/api/status")  // Le même objet est aussi renvoyé

L'appel est synchrone : la requête est routée à l'intérieur du moteur, et l'objet {statusCode, body} est à la fois transmis au rappel et renvoyé. path doit commencer par /. Un body de type chaîne est envoyé tel quel ; toute autre valeur est encodée en JSON. Comme la requête ne passe jamais par l'écouteur, aucun jeton n'intervient et l'API n'a pas besoin d'être activée. POST /api/scripts/run est refusé avec 409 et would_reenter — il lui faudrait le moteur que le script appelant occupe — et une route qui ne répond pas dans les 12 secondes donne 504.

$script — métadonnées du script

$script.name       // Nom du script issu de la configuration
$script.type       // Chaîne de type du script
$script.startTime  // Horodatage monotone (secondes depuis la référence de démarrage système, et non l'Epoch)
$script.sessionID  // Différent à chaque exécution, pour relier l'état d'un même passage

Variables globales par exécution

Les variables suivantes sont injectées à chaque exécution de script et sont propres à certains types de scripts.

$argument — argument du script

La valeur chaîne du paramètre argument= de la configuration du script, ou null lorsque le script n'en a pas. Disponible dans : http-request, http-response, http-request-before-send, rule, dns, cron, event.

console.log("Argument: " + $argument)

$domain — domaine DNS (scripts DNS uniquement)

Le nom de domaine interrogé. Disponible uniquement dans les scripts dns.

var domain = $domain  // par ex. "example.com"

$cronexp — expression cron (scripts cron uniquement)

L'expression de planification cron issue de la configuration du script. Disponible uniquement dans les scripts cron.

console.log("Schedule: " + $cronexp)  // par ex. "*/30 * * * *"

$event — informations d'événement (scripts event uniquement)

Informations sur l'événement déclencheur. Chute déclenche network-changed, engine-started et profile-reloaded — voir Script d'événement.

console.log("Event: " + $event.name)  // "network-changed"

$event.name est l'événement qui s'est réellement déclenché. Un script portant event-name= ne s'exécute jamais que pour cet événement-là, si bien que le nom est toujours celui qu'il a déclaré ; un script sans event-name= s'exécute pour les trois, et $event.name est ce qui lui permet de les distinguer.

console — journalisation

console.log("Debug message")     // Journal détaillé
console.warn("Warning message")  // Journal d'avertissement
console.error("Error message")   // Journal d'avertissement marqué comme erreur JavaScript

Chaque message est tronqué à 256 caractères, et les identifiants qu'il contient — un en-tête Authorization ou Cookie, une valeur password= ou token= et consorts — sont consignés sous la forme <redacted>. console.log écrit au niveau verbose : il n'apparaît donc que lorsque loglevel vaut verbose ; console.warn et console.error apparaissent au niveau par défaut, warning.

setTimeout(fn, seconds) — minuteur

Planifie l'exécution d'une fonction après un délai.

setTimeout(function() {
    console.log("Delayed execution")
}, 2.5)  // 2,5 secondes

$scriptImport(subScriptPath) — chargeur de sous-script

Charge et évalue un autre fichier JavaScript. Seuls les chemins de fichiers locaux sont pris en charge ; les URL http(s):// et file:// sont rejetées. ($script est réservé à l'objet de métadonnées du script.)

$scriptImport("/path/to/helper.js")

// Échangez des données entre scripts au moyen de $persistentStore

Détails par type de script

Les trois types de scripts HTTP ne voient une requête HTTP en clair que lorsqu'elle parvient à Chute par son proxy HTTP, et une requête HTTPS que lorsque son hôte est déchiffré — ajoutez l'hôte à hostname dans Déchiffrement HTTPS.

Script de requête HTTP

Exécuté à la réception des en-têtes de requête. Peut modifier l'URL, les en-têtes et le corps avant la transmission de la requête.

[Script]
ModifyHeaders = type=http-request, script-path=modify.js, pattern=^https://api\.example\.com

Script de réponse HTTP

Exécuté à la réception des en-têtes de réponse. Peut modifier le statut, les en-têtes et le corps avant le renvoi au client.

[Script]
ModifyResponse = type=http-response, script-path=response.js, pattern=^https://api\.example\.com

Un script qui reçoit le corps s'exécute une fois celui-ci arrivé en entier. En HTTP/1.x, si le serveur ferme la connexion avant, un corps sans Content-Length ni encodage par blocs est complet — la fermeture en marque la fin — et le script s'exécute comme d'habitude. Un corps délimité par Content-Length ou par l'encodage par blocs que la fermeture a tronqué n'est pas transmis au script : il part vers le client exactement tel qu'il est arrivé, puis la connexion est fermée. Une règle de réécriture de corps qui correspond au même message est appliquée d'abord, et le script voit le corps réécrit.

Script de requête HTTP avant envoi

Exécuté juste avant l'envoi de la requête vers l'amont. Quand le corps est retenu — ce script a requires-body=true, ou une règle de réécriture de corps ou un script http-request reçoit le corps —, le script s'exécute une fois le corps arrivé en entier. Une requête sans corps, ou dont le corps dépasse max-size, n'est pas retenue, et un corps qui dépasse max-size pendant qu'il est retenu repart tel qu'il est arrivé : dans les deux cas, le script s'exécute au départ de l'en-tête avec un corps vide, et un script avec requires-body=true est ignoré. Utile pour modifier les corps de requête POST/PUT. Il s'exécute en dernier : après toute règle de réécriture de corps et tout script http-request qui reçoit le corps, et il voit le corps qu'ils ont produit. Un corps renvoyé par le script remplace celui de la requête, qui est abandonné, et Content-Length est ajusté en conséquence.

[Script]
BeforeSend = type=http-request-before-send, script-path=before-send.js, pattern=^https://api\.example\.com, requires-body=true

Script de règle

Correspondance de règle personnalisée. Le script doit appeler $done({matched: true}) ou $done({matched: false}).

[Rule]
SCRIPT,MyRuleScript,DIRECT

[Script]
MyRuleScript = type=rule, script-path=rule.js

Script DNS

Résolution DNS personnalisée. Reçoit $domain et renvoie la ou les adresses résolues.

// dns.js
var domain = $domain
if (domain === "internal.example.com") {
    $done({address: "10.0.0.1", ttl: 300})
} else {
    $done({})  // Passe-plat vers la résolution DNS normale
}

Une entrée [Host] <domain> = script:<name> confie la résolution des domaines correspondants au script DNS nommé — voir Mappage DNS local.

Script cron

Exécution planifiée à l'aide d'expressions cron. L'intervalle minimal est de 60 secondes.

[Script]
HalfHourlyTask = type=cron, script-path=task.js, cron-expression=*/30 * * * *

L'expression doit compter exactement cinq champs séparés par des espaces simples. Toute autre écriture — double espace, tabulation, quatre champs, six champs — désactive le script en silence : il n'est jamais planifié, et rien n'est écrit dans le journal.

Des cinq champs, seul celui des minutes est pris en compte : */N s'exécute toutes les N minutes. Tout autre champ des minutes se déclenche toutes les 60 secondes, et le script doit alors vérifier lui-même l'heure courante pour décider d'agir ou non.

Script d'événement

Déclenché par des événements système. Trois événements sont déclenchés :

Événement Quand il se déclenche
network-changed Le réseau Wi-Fi ou cellulaire a changé
engine-started Le moteur a fini de démarrer — politiques, règles et écouteurs sont actifs
profile-reloaded Un rechargement de configuration s'est terminé, sur les scripts du profil rechargé
[Script]
NetChange = type=event, script-path=network-changed.js

L'objet $event est disponible :

$event.name  // "network-changed", "engine-started" ou "profile-reloaded"

Surge désigne l'événement avec event-name=, et Chute le lit aussi : event-name=engine-started n'exécute le script que sur cet événement. Un script qui ne nomme aucun événement s'exécute sur les trois et lit $event.name pour savoir lequel s'est déclenché. Un script lié à un autre événement — Surge en déclenche aussi, comme notification — ne s'exécute jamais dans Chute, et le journal le signale au chargement de la configuration.


Exemples pratiques

Rediriger les appareils mobiles

Un script http-request qui redirige les utilisateurs mobiles d'après l'User-Agent :

[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({})
}

Bloquer du contenu dans les réponses d'API

Un script http-response qui retire les publicités et le contenu sponsorisé d'une réponse d'API JSON :

[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)})

Modifier le corps d'une requête avant l'envoi

Un script http-request-before-send qui assainit une charge utile POST :

[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)})

Règle personnalisée : routage selon l'heure

Un script rule qui sélectionne un proxy différent selon l'heure de la journée :

[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})  // Poursuit vers la règle suivante pendant les heures de travail
} else {
    $done({matched: true})   // Utilise ProxyA en dehors des heures de travail
}

DNS personnalisé pour les domaines internes

Un script dns qui résout les noms d'hôte internes vers des IP locales :

[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({})  // Passe-plat vers le DNS normal
}

Basculer automatiquement de politique lors d'un changement de réseau

Un script event qui teste la connectivité après un changement de réseau et bascule le groupe de politiques en conséquence :

[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) {
            // Le test a échoué sur le nouveau réseau — bascule vers le groupe de secours
            $klne.selectPolicy("MainGroup", "BackupProxy")
            console.log("Probe failed after " + $event.name)
        } else {
            $klne.selectPolicy("MainGroup", "MainProxy")
        }
        $done({})
    })

Remarque : $network est visible depuis tous les scripts, mais son wifi.ssid n'est renseigné que sous iOS et Android (voir la remarque dans $network). Sous macOS et tvOS le nom est vide ; un script d'événement ou un script cron qui doit fonctionner partout teste donc avec $httpClient comme ci-dessus au lieu de lire le SSID.

Vérification d'état périodique

Un script cron qui vérifie l'état du proxy toutes les 30 minutes :

[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("Health check failed: " + (error || "status " + response.status))
            $notification.post("Chute Alert", "Health Check", "Cannot reach Google")
        } else {
            console.log("Health check OK")
        }
        $done({})
    }
)

Appelez $done({}) à l'intérieur du rappel — achever le script annule toutes les requêtes $httpClient en attente ; un $done() synchrone en fin de script annulerait donc la vérification d'état avant l'arrivée de sa réponse. Pour la même raison, le timeout propre à une requête doit être plus court que celui du script : lorsque le timeout du script (5 secondes par défaut) expire le premier, la requête est annulée et son rappel ne s'exécute jamais. Les deux exemples ci-dessus fixent timeout=15 sur la ligne [Script].

Enrichir les réponses d'API avec des données externes

Un script http-response qui enrichit les données utilisateur en appelant une API secondaire :

[Script]
EnrichUsers = type=http-response, script-path=enrich.js, pattern=^https://api\.example\.com/users, requires-body=true, timeout=20
// enrich.js — au plus 8 requêtes en cours, la limite par script
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()

Modèle d'exécution

  • Tous les scripts s'exécutent sur une file série dédiée, pour la sûreté vis-à-vis des threads.
  • Chaque exécution de script dispose d'un délai propre au script, compté depuis le début de l'exécution : le chargement de la source (un script distant est téléchargé dans ce délai), l'exécution du code et l'attente des minuteurs et des requêtes se partagent tous ce délai. Si $done() n'est pas appelé dans ce délai, le script est traité comme un passe-plat. Un script qui rend la main sans $done() alors qu'aucune minuteur setTimeout ni aucune requête $httpClient n'est en attente est traité en passe-plat aussitôt.
  • Un pool de 3 contextes préchauffés est maintenu ; un contexte est mis au rebut après chaque exécution et remplacé par un neuf, de sorte que les variables globales ne fuient jamais d'une exécution à l'autre.
  • Chute relève l'utilisation mémoire du processus au démarrage du moteur de scripts. Dès qu'elle a augmenté de plus de 10 Mo sous iOS et tvOS, ou de 512 Mo sous macOS, chaque script est ignoré et son message est transmis tel quel ; sous Android, la mesure est le tas Java utilisé, avec une marge de 512 Mo. La marge vaut pour l'ensemble du processus, pas pour un seul script, et le point de départ n'est pas relevé de nouveau tant que Chute tourne.
  • Au plus 16 exécutions de scripts peuvent être en cours simultanément sous iOS, tvOS et Android (64 sous macOS), avec au plus 4 Mo de source de script pour l'ensemble (8 Mo sous macOS). Une exécution qui dépasse l'une ou l'autre limite est ignorée et son message est transmis tel quel ; le journal indique execution admission is full.
  • La source d'un script ne peut dépasser 1 Mo sous macOS et 512 Ko sous iOS, tvOS et Android. Le nombre de scripts qu'une configuration peut déclarer n'est pas limité ; ce qui est plafonné, c'est le nombre de sources de scripts en cours de chargement simultané — 32 sous macOS, 8 sous iOS, tvOS et Android. Au-delà du plafond, le journal indique source load queue is full et cette exécution-là se fait sans source, ce qui revient à un passe-plat.
  • Les scripts distants (chemins HTTP/HTTPS) sont récupérés à chaque exécution. Un script-update-interval positif, ou valant exactement -1, amène en outre Chute à interroger l'URL par une requête HEAD conditionnelle toutes les 10 minutes. La valeur n'est qu'un interrupteur, pas une période : l'interrogation a lieu toutes les 10 minutes quel que soit le nombre. 0 (la valeur par défaut) et toute autre valeur négative laissent l'interrogation désactivée.

Intégration des scripts de module

Les scripts peuvent également être définis dans les fichiers Module (.sgmodule), sous la section [Script].

S. Smart Rabbit LLC © All Rights Reserved            updated 2026-09-30 15:11:19

results matching ""

    No results matching ""