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 utilisent le moteur JavaScriptCore d'Apple 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=262144, 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 |
Oui | 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 |
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é) |
timeout |
Non | 5,0 secondes | Délai d'exécution par script. Les valeurs supérieures à 30 secondes sont ramenées à 30 |
argument |
Non | — | Argument chaîne personnalisé, disponible en JS via $argument |
debug |
Non | false | Réservé ; sans effet actuellement |
cron-expression |
Non | — | Expression de planification cron (type cron uniquement) |
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) |
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 après collecte du corps complet, avant l'envoi |
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) |
Détection automatique du corps : si la source du script contient
$request.bodyou$response.body, le corps sera fourni automatiquement dans la limite demax-size. Utilisezrequires-body=truepour forcer ce comportement.
Référence de l'API JavaScript
Les scripts s'exécutent dans un environnement JavaScriptCore isolé où les objets globaux suivants sont disponibles.
$request (lecture seule)
Disponible dans : http-request, http-response, http-request-before-send, rule
| Propriété | Type | Description |
|---|---|---|
.url |
String | URL complète de la requête |
.method |
String | Méthode HTTP (GET, POST, etc.) ou QUERY pour le DNS |
.headers |
Object | En-têtes de requête sous forme de paires clé-valeur |
.body |
String ou null | Corps de la requête (décodé en UTF-8) |
.bodyBytes |
Uint8Array ou null | Octets bruts du corps de la requête |
.hostname |
String | Nom d'hôte cible |
.destPort |
Number | Port de destination |
.processPath |
String | Chemin du processus émetteur (macOS uniquement) |
.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 | En-têtes de réponse sous forme de paires clé-valeur |
.body |
String ou null | Corps de la réponse (décodé en UTF-8) |
.bodyBytes |
Uint8Array ou null | Octets bruts du corps de la réponse |
.rawBody |
Uint8Array ou null | Alias de .bodyBytes |
$done(value) — gestionnaire d'achèvement
Doit être appelé exactement une fois à la fin du script pour signaler son achèvement. L'exécution du script est bloquée jusqu'à l'appel de $done() ou l'expiration du 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)
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 sans contacter le serveur amont. C'est utile pour bloquer, simuler des API ou renvoyer du contenu en cache.
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" // Déclenche une redirection 302
})
Lorsque url est fourni, Chute renvoie une redirection 302 vers l'URL indiquée au lieu de la réponse d'origine. 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)
Les clés
server/serverssont acceptées mais actuellement ignorées ; la résolution se poursuit via le DNS normal.
$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)
Signature du rappel : callback(error, response, data)
error: chaîne d'erreur ounullresponse:{status: Number, headers: Object}ounulldata: corps de réponse sous forme de chaîne UTF-8, ounull
$httpClientest disponible dans les scriptshttp-request,http-response,http-request-before-send,cronetevent. Il n'est pas disponible dans les scriptsruleetdns.
$persistentStore — stockage clé-valeur
Stockage clé-valeur persistant qui survit aux redémarrages du script et du processus. Repose sur NSUserDefaults.
$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.
$notification.post("Title", "Subtitle", "Notification body text")
$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}
bssidvaut toujoursnull;ssidn'est renseigné que sur iOS.Remarque : dans les versions actuelles, les propriétés
$networket$environmentne sont pas visibles depuis les scripts en raison d'une limitation du pont et valentundefined. Les métadonnées$scriptne sont disponibles que dans les scripts cron et event.
$environment — informations d'exécution
$environment.system // "iOS" ou "macOS"
$environment.appVersion // Chaîne de version du SDK KLNEKit
$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.ungzip(data) // Décompresse des données gzip
$klne — API de contrôle du proxy
Pilote le moteur du proxy depuis les scripts. $klne est disponible dans tous les types de scripts.
$klne.getPolicyGroups() // Récupère tous les groupes de politiques
$klne.selectPolicy("Group", "Proxy") // Change la politique d'un groupe
$klne.getActiveConnections() // Liste les connexions actives
$klne.closeConnection("id") // Réservé — sans effet actuellement
$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
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 — politiques candidates
selectedIndex: 0, // Number — index de la politique active (absent si non résolue)
selectedPolicy: "A" // String — nom de la politique active (absent si non résolue)
}]
}
getActiveConnections() renvoie un tableau décrivant les connexions en cours :
[{
id: "0x600002f01230", // String — identifiant opaque de connexion
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 depolicy(un nom issu despolicyNamesdu groupe) la politique active degroup. Un groupe ou un nom de politique inconnu est ignoré avec un avertissement dans le journal.closeConnection(id)— réservé. Le noyau actuel ne peut pas fermer de connexions individuelles ; l'appeler ne fait que consigner un avertissement, sans rien fermer.flushDNS()— purge le cache DNS.startURLTest(group)— démarre un test de latence asynchrone pour un groupeurl-testoufallback; les autres types de groupes et les noms inconnus sont ignorés avec un avertissement.reloadConfiguration()— recharge la configuration actuelle.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.
$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)
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. 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. Seul network-changed est pris en charge actuellement.
console.log("Event: " + $event.name) // "network-changed"
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
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
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
Script de requête HTTP avant envoi
Exécuté une fois le corps complet de la requête collecté, juste avant l'envoi vers l'amont. Utile pour modifier les corps de requête POST/PUT.
[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
}
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 * * * *
Seul le champ des minutes de l'expression est pris en compte : */N s'exécute toutes les N minutes. Toute autre expression 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. Prend actuellement en charge l'événement network-changed (déclenché lorsque le réseau Wi-Fi ou cellulaire change).
[Script]
NetChange = type=event, script-path=network-changed.js
L'objet $event est disponible :
$event.name // "network-changed"
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
// 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 : lire le SSID Wi-Fi via
$networkserait ici la solution naturelle, mais$networkn'est pas encore visible depuis les scripts (voir la remarque dans $network) ; le test avec$httpClientfonctionne dans les versions actuelles.
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, 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$httpClienten attente ; un$done()synchrone en fin de script annulerait donc la vérification d'état avant l'arrivée de sa réponse.
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
// enrich.js
var users = JSON.parse($response.body)
var pending = users.length
if (pending === 0) { $done({}) }
users.forEach(function(user, index) {
$httpClient.get("https://internal-api.example.com/avatar/" + user.id,
function(error, resp, data) {
if (!error && resp.status === 200) {
users[index].avatar = JSON.parse(data).url
}
pending--
if (pending === 0) {
$done({body: JSON.stringify(users)})
}
}
)
})
Modèle d'exécution
- Tous les scripts s'exécutent sur une file d'attente sérielle dédiée, pour la sécurité vis-à-vis des threads.
- Chaque exécution de script dispose d'un délai propre au script ; si
$done()n'est pas appelé dans ce délai, le script est traité comme un passe-plat. - Un pool de 3 JSContext 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.
- Sur iOS/tvOS, les scripts sont soumis à une limite mémoire de 10 Mo ; sur macOS, de 512 Mo.
- Les scripts distants (chemins HTTP/HTTPS) sont récupérés à chaque exécution. Lorsque
script-update-intervalest non nul, Chute interroge en outre l'URL par une requête HEAD conditionnelle toutes les 10 minutes ;0(la valeur par défaut) désactive cette interrogation.
Intégration des scripts de module
Les scripts peuvent également être définis dans les fichiers Module (.sgmodule), sous la section [Script].