JavaScript-скриптинг
Chute поддерживает JavaScript-скриптинг для расширенной модификации запросов/ответов, пользовательского сопоставления правил, разрешения DNS и запланированных задач. Скрипты выполняются на JavaScriptCore на платформах Apple и на QuickJS на Android и следуют Surge-совместимому API скриптов.
Скрипты определяются в разделе [Script] файла конфигурации.
Конфигурация
[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 * * * *
Параметры скриптов
| Параметр | Обязательно | По умолчанию | Описание |
|---|---|---|---|
type |
Нет | http-request |
Тип триггера скрипта (см. ниже) |
script-path |
Да | — | Локальный путь к файлу или HTTP(S) URL к JS-скрипту |
pattern |
Нет | (совпадает со всем) | URL Regex для фильтрации запуска скрипта. Он сопоставляется с любым местом полного URL запроса — https://… после расшифровки, http://… для обычного HTTP — и никогда не сопоставляется с одним лишь путём, поэтому шаблон вроде ^/api не совпадает |
requires-body |
Нет | автоопределение | Принудительно передавать скрипту полное тело запроса/ответа |
max-size |
Нет | 131072 (128 КБ) | Максимальный размер тела в байтах для скриптов с доступом к телу. Если собранное тело превышает этот размер, скрипт пропускается для этого запроса (тело не усекается). На HTTP/1.x — обычном HTTP и расшифрованном HTTP/1.1 — Chute буферизует для скриптов не более 131072 байт, что бы здесь ни было указано, так что там этот параметр может только понизить предел; расшифрованное сообщение HTTP/2 передаётся целиком и проверяется только по max-size |
timeout |
Нет | 5.0 секунд | Тайм-аут одного выполнения, отсчитываемый с его начала: загрузка исходного кода (для удалённого скрипта — его скачивание), выполнение кода и ожидание таймеров и запросов — всё укладывается в него. Значения выше 30 секунд ограничиваются 30 |
argument |
Нет | — | Пользовательский строковый аргумент, доступный как $argument в JS |
full-header-mode |
Нет | false | При true $request.headers и $response.headers — не объекты, а массивы {field, value}, по одному на строку заголовка (формат Surge). Действует для скриптов http-request, http-request-before-send и http-response; $done() принимает обе формы в любом случае. См. Заголовки из нескольких строк |
debug |
Нет | false | Зарезервировано; в настоящее время не имеет эффекта |
cron-expression |
Нет | — | Выражение расписания cron (только для типа cron); cronexp= и написание Shadowrocket cronexpr= принимаются как тот же ключ |
event-name |
Нет | (все события) | Событие, которое ждёт скрипт event, — network-changed, engine-started или profile-reloaded; без имени скрипт запускается на всех трёх (см. Скрипт Event) |
wake-system |
Нет | false | Зарезервировано; разбирается, но в настоящее время не имеет эффекта |
enable |
Нет | true | Включить или отключить этот скрипт |
script-update-interval |
Нет | 0 | Опрос обновлений удалённого скрипта (см. Модель выполнения) |
binary-body-mode |
Нет | — | Принимается для совместимости с Surge (также в написании binary-mode) и сохраняется при сохранении профиля, но ничего не меняет: сырые байты всегда находятся в .bodyBytes |
engine |
Нет | — | Принимается и сохраняется, но игнорируется: каждый скрипт выполняется на собственном движке платформы |
Типы скриптов
| Строка типа | Enum | Описание |
|---|---|---|
http-request |
HTTP Request | Перехват и изменение HTTP-запросов до отправки |
http-response |
HTTP Response | Перехват и изменение HTTP-ответов до клиента |
http-request-before-send |
HTTP Request Before Send | Изменение запроса непосредственно перед отправкой |
rule |
Rule | Пользовательская логика сопоставления правил |
dns |
DNS | Пользовательское разрешение DNS |
cron |
Cron | Запланированные/периодические скрипты |
event |
Event | Обработчики системных событий (например, network-changed) |
generic |
Generic | Принимается для совместимости с Surge; не привязан к запросам, правилам, DNS или событиям, поэтому Chute никогда не запускает его сам. Запустите его кнопкой Запустить в списке скриптов Chute iOS или Chute Android либо через POST /api/scripts/run в HTTP API управления |
Неизвестный type= отмечается в журнале и обрабатывается как http-request.
Автоопределение тела: для скриптов
http-requestиhttp-responseисходный код, содержащий$request.bodyили$response.body, автоматически получает тело доmax-size. Используйтеrequires-body=trueдля принудительного включения этого поведения. Скрипт, который читает только.bodyBytesили.rawBody, не распознаётся, а скриптыhttp-request-before-sendне распознаются никогда: им нуженrequires-body=true.
Справочник JavaScript API
Скрипты выполняются в изолированной среде JavaScript — JavaScriptCore на платформах Apple, QuickJS на Android — со следующими доступными глобальными объектами.
$request (Только чтение)
Доступно в: http-request, http-response, http-request-before-send, rule, dns
| Свойство | Тип | Описание |
|---|---|---|
.url |
String | Полный URL запроса; порт указывается, если он не является портом схемы по умолчанию, а хост IPv6 заключается в квадратные скобки. Цель запроса, которая уже является абсолютным URL, используется как есть, перед целью, не начинающейся с /, ставится /, а если хост неизвестен, значение — только путь. В скрипте rule это хост назначения, в скрипте dns — запрашиваемый домен |
.method |
String | HTTP-метод (GET, POST и др.) или QUERY для DNS |
.headers |
Object или Array | Заголовки запроса: каждое имя со своим значением; значения заголовка из нескольких строк объединяются в одну строку (см. Заголовки из нескольких строк). С full-header-mode=true — массив {field, value}. Имена находятся независимо от регистра (см. Имена заголовков) |
.body |
String или null | Тело запроса (в кодировке UTF-8) |
.bodyBytes |
Bytes или null | Сырые байты тела запроса (см. примечание ниже) |
.hostname |
String | Целевое имя хоста |
.destPort |
Number | Порт назначения |
.processPath |
String | Путь запрашивающего процесса (macOS; на Android — путь к APK приложения, для TCP-соединений, которые перехватывает VPN) |
.userAgent |
String | Значение заголовка User-Agent |
.sourceIP |
String | IP-адрес источника |
.listenPort |
Number | Порт прослушивания прокси |
.requestId |
String | Уникальный идентификатор запроса |
.dnsResult |
String | Разрешённый IP-адрес |
.srcPort |
Number | Порт источника |
.protocol |
String | Определённый протокол: http, https, tcp, dns |
$response (Только чтение)
Доступно в: http-response
| Свойство | Тип | Описание |
|---|---|---|
.status |
Number | Код статуса HTTP |
.headers |
Object или Array | Заголовки ответа: каждое имя со своим значением; значения заголовка из нескольких строк объединяются в одну строку (см. Заголовки из нескольких строк). С full-header-mode=true — массив {field, value}. Имена находятся независимо от регистра (см. Имена заголовков) |
.body |
String или null | Тело ответа (в кодировке UTF-8) |
.bodyBytes |
Bytes или null | Сырые байты тела ответа (см. примечание ниже) |
.rawBody |
Bytes или null | Псевдоним для .bodyBytes |
.body,.bodyBytesи.rawBodyсодержат одно и то же тело: прежде чем его увидит скрипт, разбиение chunked снимается, аContent-Encodinggzip или deflate раскрывается..bodyBytesи.rawBodyсодержат байтовый объект. Передайте его в вызов, принимающий байты: в$utils.ungzip(), результат которого — тоже байтовый объект, или вbodyу$done(), который принимает его как сырое тело. Чем именно является этот объект, зависит от движка: на платформах Apple это непрозрачная обёртка без.lengthи без индексации, на Android —Uint8Array, поэтому скрипт, рассчитанный на все платформы, не должен полагаться на.lengthили индексацию. Чтобы прочитать содержимое, используйте.body— те же байты, декодированные как UTF-8.
$done(value) — Обработчик завершения
Должен быть вызван ровно один раз в конце скрипта для сигнализации о завершении. Скрипт, который вернул управление, не вызвав его, немедленно завершается как пропуск — если только не осталось незавершённого таймера setTimeout или запроса $httpClient. timeout= отсчитывается с момента начала выполнения: в него входят загрузка исходного кода, выполнение кода и это ожидание.
$done({}) // Пропуск — без изменений
$done() // Прервать соединение
$done({matched: true}) // Результат сопоставления правила (только для rule-скриптов)
$done({address: "1.2.3.4"}) // Результат DNS (только для dns-скриптов)
В скрипте http-request, http-request-before-send или http-response вызов $done() без аргумента — или со значением, которое не является объектом, — прерывает соединение: Chute ничего не отправляет вместо запроса или ответа, а прерванный запрос не доходит до сервера. То, что уже отправляется клиенту, сначала дописывается, затем соединение сразу закрывается. В HTTP/2 сбрасывается только поток этого запроса (RST_STREAM с кодом ошибки CANCEL), и ничего не отправляется вместо него; соединение и другие запросы в нём продолжают работать.
Заголовки и url из скрипта проверяются перед использованием. Заголовок отбрасывается с предупреждением в журнале, если:
- в его имени или значении есть перевод строки (CR или LF) или символ NUL:
[JS] Dropped header <name>: a header name or value cannot contain a line break; - его имя не является допустимым именем поля HTTP (RFC 9110 §5.1). Допустимое имя состоит из одной или нескольких букв, цифр или символов из
!#$%&'*+-.^_`|~, поэтому пустое имя или имя с пробелом, двоеточием или буквой не из ASCII отбрасывается:[JS] Dropped header <name>: not a valid header name.
Остальная часть результата применяется. Те же проверки действуют для заголовков, заданных в форме массива, и для заголовков response, который возвращает скрипт запроса. url с переводом строки, NUL или пробелом игнорируется, тоже с предупреждением. Путь и строка запроса url отправляются в том виде, в каком написаны: экранирование вроде %20 не раскрывается.
Возвращаемые значения скрипта HTTP Request:
$done({
url: "https://new.example.com/path", // Перезаписать URL
headers: {"X-Custom": "value"}, // Изменить заголовки
body: "new request body", // Изменить тело
response: { // Вернуть синтетический ответ (пропустить отправку)
status: 200,
headers: {"Content-Type": "text/html"},
body: "<html>Blocked</html>"
}
})
Когда указан response, запрос замыкается: Chute возвращает синтетический ответ напрямую клиенту, а запрос не отправляется на исходный сервер. Так происходит везде, где Chute обрабатывает запрос, включая HTTP/2, и независимо от того, работает ли скрипт на заголовке, на полном теле или как http-request-before-send. В HTTP/1.x строка состояния несёт стандартную поясняющую фразу кода (HTTP/1.1 404 Not Found), а соединение закрывается, как только ответ отправлен; в HTTP/2 отвечается только поток этого запроса. status — число от 200 до 599 или строка, которая после удаления пробелов по краям является таким целым числом ("404"); любое другое значение или его отсутствие даёт 200. Content-Length вычисляется по body, что бы ни было в headers. Это полезно для блокировки, имитации API или возврата кэшированного контента.
Ответы, которые Chute формирует сам, — имитация ответов (Map Local),
responseскрипта, а также ответы и страница ошибки перезаписи URL и политик REJECT — следуют HTTP: ответ на запрос HEAD — это только заголовок, с тем Content-Length, который получил бы GET, а ответ со статусом 204, 205 или 304 не несёт ни тела, ни Content-Length.
Возвращаемые значения скрипта HTTP Response:
$done({
status: 200, // Изменить код статуса
headers: {"X-Custom": "value"}, // Изменить заголовки ответа
body: "new response body", // Изменить тело ответа
url: "https://other.example.com" // Редирект (по умолчанию 302)
})
body заменяет тело ответа и больше ничего: строка состояния и заголовки остаются серверными, с влитыми в них возвращёнными status и headers, а собственное тело сервера отбрасывается. Если скрипт работал только на заголовке, после него, как обычно, выполняются правила перезаписи тела и скрипты http-response, получающие тело, — уже над новым телом. У ответа на HEAD и у ответа со статусом 1xx, 204, 205 или 304 тела нет, поэтому возвращённый body там игнорируется, а status и headers по-прежнему применяются.
status — число от 200 до 599 или строка, которая после удаления пробелов по краям является таким целым числом ("301"). Любое другое значение, включая true и false, игнорируется, и ответ сохраняет свой статус. Если status меняет код, строка состояния получает и стандартную поясняющую фразу нового кода. При status 204, 205 или 304 ответ уходит без тела — ни серверного, ни возвращённого body — и без заголовка длины; в HTTP/1.x соединение закрывается, как только отправлен заголовок.
Когда указан url, Chute вместо исходного ответа отвечает редиректом на него. Его статус — возвращённый status (например, 301, 307 или 308) или 302, если статус не задан или проигнорирован; возвращённые headers вливаются как обычно, а Location — это url. Без body у редиректа нет тела, и тело сервера не отправляется; в HTTP/2 не отправляются и трейлеры сервера. url должен возвращаться вместе как минимум с одним из status / headers / body; результат, содержащий только url, обрабатывается как пропуск.
Возвращаемые значения скрипта DNS:
$done({address: "1.2.3.4"}) // Один IP
$done({addresses: ["1.2.3.4", "5.6.7.8"]}) // Несколько IP
$done({address: "10.0.0.1", ttl: 300}) // С пользовательским TTL (секунды, по умолчанию 60)
$done({server: "1.1.1.1"}) // Разрешить через этот сервер
$done({servers: ["tls://dns.google", "8.8.8.8#disable-ipv6"]})
server/serversне дают ответ, а выбирают резолвер. Каждая запись — обычная запись DNS-сервера: адрес,tls://,https://, с привычными параметрами после#. Их спрашивают вместе, побеждает первый ответ. Если ни одна запись не пригодна, разрешение завершается неудачей, а не возвращается к настроенному пулу.
Заголовки из нескольких строк
Заголовок может занимать несколько строк: ответ, который устанавливает два cookie, несёт две строки Set-Cookie. Скрипты http-request, http-request-before-send и http-response могут прочитать каждую строку и записать несколько.
Чтение
По умолчанию $request.headers и $response.headers — объекты, где каждому имени заголовка соответствует одна строка. Если у заголовка несколько строк, их значения объединяются по порядку: значения Cookie — через "; ", значения любого другого заголовка (в том числе Set-Cookie) — через ", ".
$response.headers["Set-Cookie"] // "a=1; Path=/, b=2; Path=/"
$request.headers["Cookie"] // "a=1; b=2"
Дата Expires в cookie содержит запятую, поэтому объединённую строку Set-Cookie нельзя надёжно разбить обратно на отдельные cookie. Чтобы читать каждую строку отдельно, добавьте full-header-mode=true в строку скрипта. Тогда $request.headers и $response.headers — массивы с одним объектом {field, value} на строку, в формате Surge. Строки с одинаковым именем сохраняют свой порядок.
// 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=/"]
То же действует и в HTTP/2: если клиент разбивает cookie на несколько полей cookie, скрипт читает их как один Cookie, объединённый через "; ", а каждый set-cookie — отдельная строка.
Запись
headers в $done() меняет только те заголовки, которые называет; все остальные остаются как есть. Те же правила действуют для headers внутри response, которым отвечает скрипт запроса.
| Значение | Действие |
|---|---|
| строка | У заголовка остаётся ровно одна строка с этим значением; все его прежние строки заменяются |
| массив строк | По строке на каждое значение, по порядку, вместо строк заголовка. {"Set-Cookie": ["a=1", "b=2"]} отправляет две строки Set-Cookie |
[] |
Заголовок удаляется |
что-либо другое (число, null, объект, массив с элементом не-строкой) |
Игнорируется: заголовок остаётся как есть |
Объединённая строка, которую скрипт возвращает ровно такой, какой прочитал, сохраняет исходные строки заголовка. Поэтому const h = $response.headers; h["X-A"] = "1"; $done({headers: h}) меняет только X-A, а две строки Set-Cookie по-прежнему уходят двумя строками. Любая другая строка заменяет все строки заголовка одной. Чтобы добавить cookie, запишите весь список массивом: прочитайте имеющиеся строки в режиме full-header, затем верните {"Set-Cookie": [...existing, "c=3"]}.
Запрос несёт одну строку Cookie: массив, заданный для Cookie в headers скрипта http-request или http-request-before-send, объединяется через "; ", так что {"Cookie": ["a=1", "b=2"]} отправляет Cookie: a=1; b=2.
$done() принимает headers и в виде массива объектов {field, value}, с full-header-mode или без него:
$done({headers: [{field: "Set-Cookie", value: "a=1"},
{field: "Set-Cookie", value: "b=2"},
{field: "X-A", value: "1"}]})
Элементы группируются по имени без учёта регистра, и каждая группа заменяет строки своего заголовка в заданном порядке. Заголовки, которых нет в массиве, остаются как есть, так что пропуск заголовка его не удаляет; чтобы удалить заголовок, используйте форму объекта с []. Элемент без строкового field и строкового value игнорируется.
- Имена заголовков не зависят от регистра:
set-cookieиSet-Cookie— один и тот же заголовок, поэтому указывайте каждое имя только один раз. Имя, не являющееся допустимым именем заголовка, отбрасывается, как описано в разделе$done. - Значение с переводом строки или NUL отбрасывается, как описано в разделе
$done. В массиве отбрасывается только это значение; если отброшены все значения, заголовок остаётся как есть. - В HTTP/2 каждая строка становится отдельным полем с именем в нижнем регистре. Заголовки, относящиеся только к одному соединению, например
ConnectionиKeep-Alive, не отправляются.
Имена заголовков в $request.headers и $response.headers
$request.headers и $response.headers находят заголовок, в каком бы регистре ни было записано его имя: $response.headers['ETag'], $response.headers['etag'] и $response.headers['Etag'] читают один и тот же заголовок. Это относится и к in ('etag' in $response.headers), и к присваиванию (headers['content-type'] = 'text/html' меняет уже имеющийся в объекте Content-Type, а не добавляет для него второе имя), и к delete.
Если перечислить имена (Object.keys(), for…in, JSON.stringify()), каждый заголовок появится один раз и в том написании, в каком его хранит Chute, — каждое слово с заглавной буквы, как бы ни написали его клиент или сервер: Content-Type, Etag, X-Api-Key, Www-Authenticate.
Так сопоставляет имена только объект, прочитанный из $request.headers или $response.headers. Копия, которую вы делаете сами, например через Object.assign({}, $request.headers) или {...$response.headers}, — обычный объект; в копии используйте написание, показанное выше. С full-header-mode=true headers — массив объектов {field, value}, где field записан так же, и этот поиск не действует; сравнивайте field.toLowerCase().
$httpClient — Асинхронный HTTP-клиент
Выполнение HTTP-запросов из скриптов. Все запросы отменяются при вызове $done() или по тайм-ауту.
$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)
Объект опций может также содержать policy — имя политики, определённой в профиле: тогда запрос уходит через эту политику, а не по маршруту по умолчанию. Имя, которого профиль не определяет, записывается в журнал с предупреждением, и запрос идёт по маршруту по умолчанию. Встроенная форма Surge policy-descriptor не поддерживается: она записывается в журнал с предупреждением, и запрос тоже идёт по маршруту по умолчанию, — определите политику в профиле и передайте её имя.
Сигнатура колбэка: callback(error, response, data)
error: Строка ошибки илиnullresponse:{status: Number, headers: Object}илиnull.headersнаходит заголовок независимо от регистра имени и перечисляет имена так же, как$request.headers, — каждое слово с заглавной буквы, напримерContent-Type,Etag,X-Api-Key, — сpolicyи без негоdata: тело ответа, декодированное как строка UTF-8; тело, не являющееся корректным UTF-8, приходит байтовым объектом, как и.bodyBytes, а не какnull.null— только когда тела нет
$httpClientдоступен в скриптахhttp-request,http-response,http-request-before-send,cronиevent. Он недоступен в скриптахruleиdns.У одного скрипта может быть не более 8 незавершённых запросов, а у всех скриптов вместе — 16. Запрос сверх любого из этих пределов не отправляется, и его колбэк не вызывается никогда; журнал сообщает об этом один раз за запуск. Тело ответа больше 4 МБ приводит к ошибке запроса.
$persistentStore — Хранилище ключ-значение
Постоянное хранилище ключ-значение, сохраняющееся между перезапусками скриптов и процессов; все скрипты читают и записывают одни и те же ключи. На Android оно хранится в закрытом хранилище Chute и исключено из резервных копий устройства.
$persistentStore.write(data, key) // Сохранить значение
$persistentStore.read(key) // Получить значение
$persistentStore.remove(key) // Удалить значение
$notification — Локальные уведомления
Отправка локальных системных уведомлений. На Apple TV Chute их не показывает.
$notification.post("Title", "Subtitle", "Notification body text")
Необязательный четвёртый аргумент передаёт параметры Surge: url (ссылка, открываемая при нажатии на уведомление), action (при наличии url подразумевается open-url, и это единственное действие, которое обрабатывают приложения, — остальные, например clipboard из Surge, передаются, но их ничто не обрабатывает) и auto-dismiss (секунды). Они прикрепляются к уведомлению; остальные ключи игнорируются с предупреждением. url обрабатывают Chute iOS, Chute Mac и Chute Android: они открывают его при нажатии на уведомление. auto-dismiss обрабатывает только Chute Android: он убирает уведомление через указанное число секунд; приложения для платформ Apple оставляют его на месте.
$notification.post("Title", "", "Tap to open", {url: "https://example.com", "auto-dismiss": 5})
Уведомления скриптов — как и уведомления правил с notification-text — подчиняются переключателю уведомлений в приложении: Разрешить уведомления в Chute iOS, Показывать уведомления о событиях в Chute Mac, системное разрешение на уведомления для Chute в Chute Android. Выключите его — и ничего не показывается. Chute Android отправляет их в собственный канал уведомлений, Уведомления скриптов, который можно отдельно выключить в системных настройках уведомлений. См. Отчеты об уведомлениях.
$network — Информация о сети
Информация о состоянии сети (только чтение).
$network.dns // Массив IP-адресов DNS-серверов
$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}
ssidиbssidзаполняются на iOS, где туннель может прочитать данные сети Wi-Fi, и на Android, когда у Chute есть разрешение, которое Android требует для чтения имени сети (см. Начало работы на Android); на macOS и tvOSssidпуст, аbssidравенnull.primaryAddressпропускает собственный адрес туннеля и link-local-адреса, то есть это адрес, которым устройство выходит в сеть.carrierвсегдаnull— в iOS 16 имя оператора убрали.
$environment — Информация о среде выполнения
$environment.system // "iOS", "macOS" или "Android"
$environment.appVersion // Строка версии KLNEKit SDK
$environment.surgeVersion // Версия самого движка, под именем, которое читают скрипты Surge
$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 — Утилиты
$utils.geoip("1.2.3.4") // Код страны (например, "US")
$utils.ipasn("1.2.3.4") // Номер ASN (например, "13335")
$utils.ipaso("1.2.3.4") // Организация ASN (например, "CLOUDFLARENET")
$utils.ungzip(data) // Распаковать данные gzip
$klne — API управления прокси
Управление прокси во время выполнения из скриптов. $klne доступен в скриптах любого типа.
$klne.getPolicyGroups() // Получить все группы политик
$klne.selectGroupDetails() // Те же группы, в форме Surge
$klne.selectPolicy("Group", "Proxy") // Переключить политику для группы
$klne.getActiveConnections() // Список активных соединений
$klne.closeConnection("id") // Закрывает соединение по id, который вернул getActiveConnections()
$klne.flushDNS() // Очистить кэш DNS
$klne.startURLTest("Group") // Запустить тест URL для группы
$klne.reloadConfiguration() // Перезагрузить всю конфигурацию
$klne.setOutboundMode("rule") // Установить режим: "global"/"proxy", "direct", "rule"
$klne.setHTTPCaptureEnabled(true) // Включить/отключить MitM
$klne.setRewriteEnabled(true) // Включить/отключить семейство перезаписи
getPolicyGroups() возвращает выбираемые группы политик:
{
count: 1, // Number — число групп
policyGroups: [{
name: "MainGroup", // String — имя группы
type: 0, // Number — 0 select, 1 url-test, 2 fallback, 3 ssid, 5 load-balance
policyNames: ["A", "B"], // Array of String — фактические участники группы, включая узлы из подписок
selectedIndex: 0, // Number — индекс активной политики в policyNames (отсутствует, если не определена)
selectedPolicy: "A" // String — имя активной политики (отсутствует, если не определена)
}]
}
selectGroupDetails() возвращает те же группы в форме Surge: policyGroups сопоставляет каждому имени группы имена её участников, а decisions — каждому имени группы выбранного сейчас участника:
{
policyGroups: {MainGroup: ["A", "B"]},
decisions: {MainGroup: "A"}
}
Группа, выбор в которой не определён, в decisions не попадает.
getActiveConnections() возвращает Array с описанием текущих соединений:
[{
id: 1042, // Number — номер соединения, тот самый, который принимает closeConnection
host: "example.com", // String — хост назначения (отсутствует, если неизвестен)
port: 443 // Number — порт назначения
}]
Остальные методы принимают обычные аргументы и ничего не возвращают:
selectPolicy(group, policy)— делаетpolicy(имя изpolicyNamesгруппы) активной политикой группыgroup. Неизвестное имя группы или политики игнорируется с предупреждением в журнале.closeConnection(id)— закрывает соединение с этим id, номером, который сообщаетgetActiveConnections(). Не открытый id игнорируется с предупреждением.flushDNS()— очищает кэш DNS.startURLTest(group)— запускает асинхронный тест задержки для группыurl-test,fallbackилиload-balance; другие типы групп и неизвестные имена игнорируются с предупреждением.reloadConfiguration()— немедленно перезапрашивает источник#!MANAGED-CONFIGпрофиля, не дожидаясь интервала обновления, и применяет его, если он отличается. Повторное применение конфигурации, которая уже загружена в движок, ничего не изменит, поэтому профиль без управляемого источника лишь записывает предупреждение.setOutboundMode(mode)—"global"и"proxy"оба проксируют весь трафик,"direct"отправляет весь трафик напрямую, а любое другое значение выбирает режим по правилам.setHTTPCaptureEnabled(enabled)— включает или отключает расшифровку HTTPS (MitM) во время выполнения; принимает логическое значение.setRewriteEnabled(enabled)— включает или отключает во время выполнения всё семейство перезаписи — перезапись URL, перезапись заголовков, перезапись тела и имитацию ответов; принимает логическое значение.
$surge доступен для скриптов Surge с вызовами, значение которых совпадает: $surge.setSelectGroupPolicy(group, policy) (то же, что selectPolicy), $surge.selectGroupDetails() (то же, что selectGroupDetails), $surge.setOutboundMode(mode), $surge.setHTTPCaptureEnabled(enabled), $surge.setRewriteEnabled(enabled) (всё семейство перезаписи: перезапись URL, перезапись заголовков, перезапись тела и имитация ответов) и $surge.retestGroup(name). Остальное из $surge здесь не имеет аналога и читается как undefined, что скрипт может проверить.
$httpAPI — Мост к API управления
Вызов собственного HTTP API управления движка из скрипта. $httpAPI доступен в скриптах любого типа.
$httpAPI("GET", "/api/status", null, function(result) {
console.log(result.statusCode) // Number — статус HTTP
console.log(result.body.data) // Object — разобранное тело JSON
})
$httpAPI("/api/status") // Один аргумент: GET по этому пути
$httpAPI("DELETE", "/api/dns/cache") // Метод и путь
var result = $httpAPI("GET", "/api/status") // Тот же объект ещё и возвращается
Вызов синхронный: запрос маршрутизируется внутри движка, а объект {statusCode, body} и передаётся в колбэк, и возвращается. path должен начинаться с /. Строковое body отправляется как написано; значение любого другого типа кодируется в JSON. Поскольку запрос не проходит через слушателя, токен не участвует и API даже не обязан быть включён. POST /api/scripts/run отклоняется с 409 и would_reenter — ему потребовался бы движок, который удерживает вызывающий скрипт, — а маршрут, не ответивший за 12 секунд, даёт 504.
$script — Метаданные скрипта
$script.name // Имя скрипта из конфигурации
$script.type // Строка типа скрипта
$script.startTime // Монотонная временная метка (секунды от точки отсчета загрузки системы, не Epoch)
$script.sessionID // Уникален для каждого запуска, чтобы связать состояние одного прогона
Глобальные переменные на выполнение
Следующие переменные внедряются при каждом выполнении скрипта и специфичны для определённых типов скриптов.
$argument — Аргумент скрипта
Строковое значение из параметра argument= в конфигурации скрипта или null, если у скрипта его нет. Доступно в: http-request, http-response, http-request-before-send, rule, dns, cron, event.
console.log("Argument: " + $argument)
$domain — Домен DNS (только DNS-скрипты)
Запрашиваемое доменное имя. Доступно только в скриптах dns.
var domain = $domain // например, "example.com"
$cronexp — Выражение Cron (только Cron-скрипты)
Выражение расписания cron из конфигурации скрипта. Доступно только в скриптах cron.
console.log("Schedule: " + $cronexp) // например, "*/30 * * * *"
$event — Информация о событии (только Event-скрипты)
Информация о вызвавшем событии. Chute порождает network-changed, engine-started и profile-reloaded — см. Скрипт Event.
console.log("Event: " + $event.name) // "network-changed"
$event.name — это событие, которое действительно произошло. Скрипт с event-name= запускается только для этого одного события, поэтому имя всегда то, которое он объявил; скрипт без event-name= запускается на всех трёх, и $event.name — то, по чему он их различает.
console — Логирование
console.log("Debug message") // Подробный журнал
console.warn("Warning message") // Предупреждение
console.error("Error message") // Предупреждение, помеченное как ошибка JavaScript
Каждое сообщение обрезается до 256 символов, а учётные данные в нём — заголовок Authorization или Cookie, значение password= или token= и тому подобное — записываются в журнал как <redacted>. console.log пишет на уровне verbose, поэтому его вывод виден, только когда loglevel равен verbose; console.warn и console.error видны на уровне по умолчанию warning.
setTimeout(fn, seconds) — Таймер
Планирование выполнения функции с задержкой.
setTimeout(function() {
console.log("Delayed execution")
}, 2.5) // 2.5 секунды
$scriptImport(subScriptPath) — Загрузчик подскриптов
Загрузка и выполнение другого файла JavaScript. Поддерживаются только локальные пути к файлам; URL http(s):// и file:// отклоняются. ($script зарезервирован для объекта метаданных скрипта.)
$scriptImport("/path/to/helper.js")
// Передача данных между скриптами с использованием $persistentStore
Подробности типов скриптов
Три типа HTTP-скриптов видят обычный HTTP-запрос, только если он приходит в Chute через его HTTP-прокси, а HTTPS-запрос — только если его хост расшифровывается: добавьте хост в
hostnameв разделе Расшифровка HTTPS.
Скрипт HTTP Request
Выполняется при получении заголовков запроса. Может изменять URL, заголовки и тело до отправки запроса.
[Script]
ModifyHeaders = type=http-request, script-path=modify.js, pattern=^https://api\.example\.com
Скрипт HTTP Response
Выполняется при получении заголовков ответа. Может изменять статус, заголовки и тело до возврата клиенту.
[Script]
ModifyResponse = type=http-response, script-path=response.js, pattern=^https://api\.example\.com
Скрипт, получающий тело, запускается, когда оно пришло целиком. Если на HTTP/1.x сервер закрыл соединение раньше, тело без Content-Length и без chunked-кодирования завершено — закрытие и есть его конец, — и скрипт выполняется как обычно. Тело с Content-Length или chunked-кодированием, которое закрытие оборвало, скрипту не передаётся: оно уходит клиенту ровно в том виде, в каком пришло, после чего соединение закрывается. Правило перезаписи тела, совпавшее с тем же сообщением, применяется первым, и скрипт видит уже перезаписанное тело.
Скрипт HTTP Request Before Send
Выполняется непосредственно перед отправкой запроса. Когда тело удерживается — у этого скрипта requires-body=true или совпало правило перезаписи тела либо скрипт http-request, получающий тело, — скрипт запускается, когда тело пришло целиком. Запрос без тела или с телом больше max-size не удерживается, а тело, которое при удержании вырастает за max-size, уходит дальше как пришло: в обоих случаях скрипт запускается при отправке заголовка с пустым телом, а скрипт с requires-body=true пропускается. Полезно для изменения тел POST/PUT запросов. Он выполняется последним: после правила перезаписи тела и скрипта http-request, получающего тело, и видит тело, которое получилось после них. Тело, которое вернул скрипт, заменяет собственное тело запроса (оно отбрасывается), и Content-Length устанавливается по новому телу.
[Script]
BeforeSend = type=http-request-before-send, script-path=before-send.js, pattern=^https://api\.example\.com, requires-body=true
Скрипт Rule
Пользовательское сопоставление правил. Скрипт должен вызвать $done({matched: true}) или $done({matched: false}).
[Rule]
SCRIPT,MyRuleScript,DIRECT
[Script]
MyRuleScript = type=rule, script-path=rule.js
Скрипт DNS
Пользовательское разрешение DNS. Получает $domain и возвращает разрешённый(е) адрес(а).
// dns.js
var domain = $domain
if (domain === "internal.example.com") {
$done({address: "10.0.0.1", ttl: 300})
} else {
$done({}) // Пропуск к обычному разрешению DNS
}
Запись [Host] вида <домен> = script:<имя> передаёт разрешение подходящих доменов указанному DNS-скрипту — см. Локальное сопоставление DNS.
Скрипт Cron
Запланированное выполнение с использованием выражений cron. Минимальный интервал — 60 секунд.
[Script]
HalfHourlyTask = type=cron, script-path=task.js, cron-expression=*/30 * * * *
Выражение должно состоять ровно из пяти полей, разделённых одиночными пробелами. Всё остальное — двойной пробел, табуляция, четыре поля, шесть полей — молча отключает скрипт: он никогда не ставится в расписание, и в журнал ничего не пишется.
Из этих пяти полей учитывается только поле минут: */N выполняется каждые N минут. Любое другое поле минут срабатывает каждые 60 секунд, и скрипт должен сам проверять текущее время, чтобы решить, действовать ли.
Скрипт Event
Запускается системными событиями. Порождаются три события:
| Событие | Когда срабатывает |
|---|---|
network-changed |
Сменилась сеть Wi-Fi или сотовая |
engine-started |
Движок завершил запуск — политики, правила и слушатели подняты |
profile-reloaded |
Завершилась перезагрузка конфигурации; для собственных скриптов перезагруженного профиля |
[Script]
NetChange = type=event, script-path=network-changed.js
Объект $event доступен:
$event.name // "network-changed", "engine-started" или "profile-reloaded"
Surge задаёт событие через event-name=, и Chute тоже его читает: event-name=engine-started запускает скрипт только на этом событии. Скрипт, не называющий события, запускается на всех трёх и читает $event.name, чтобы понять, какое произошло. Скрипт, привязанный к другому событию — Surge также порождает, например, notification, — в Chute никогда не запускается, о чём журнал сообщает при загрузке конфигурации.
Практические примеры
Перенаправление мобильных устройств
Скрипт http-request, который перенаправляет мобильных пользователей на основе 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({})
}
Блокировка контента в API-ответах
Скрипт http-response, который удаляет рекламу и спонсируемый контент из JSON-ответа API:
[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)})
Изменение тела запроса перед отправкой
Скрипт http-request-before-send, который очищает 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)})
Пользовательское правило: маршрутизация по времени
Скрипт rule, который выбирает разный прокси в зависимости от времени суток:
[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}) // Переход к следующему правилу в рабочее время
} else {
$done({matched: true}) // Использовать ProxyA в нерабочее время
}
Пользовательский DNS для внутренних доменов
Скрипт dns, который разрешает внутренние имена хостов в локальные IP:
[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({}) // Пропуск к обычному DNS
}
Автоматическое переключение политики при смене сети
Скрипт event, который после смены сети проверяет связность и переключает группу политик соответственно:
[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) {
// Проверка в новой сети не удалась — переключаемся на резервную группу
$klne.selectPolicy("MainGroup", "BackupProxy")
console.log("Probe failed after " + $event.name)
} else {
$klne.selectPolicy("MainGroup", "MainProxy")
}
$done({})
})
Внимание:
$networkвиден любому скрипту, но егоwifi.ssidзаполняется только на iOS и Android (см. примечание в разделе $network). На macOS и tvOS имя пустое, поэтому скрипт event или cron, который должен работать везде, проверяет через$httpClient, как выше, а не читает SSID.
Периодическая проверка работоспособности
Скрипт cron, который проверяет работоспособность прокси каждые 30 минут:
[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({})
}
)
Вызывайте
$done({})внутри колбэка — завершение скрипта отменяет все ожидающие запросы$httpClient, поэтому синхронный$done()в конце скрипта отменил бы проверку работоспособности до получения ответа. По той же причине собственныйtimeoutзапроса должен быть короче, чем у скрипта: еслиtimeoutскрипта (по умолчанию 5 секунд) истечёт раньше, запрос отменяется и его колбэк не вызывается никогда. Оба примера выше задаютtimeout=15в строке[Script].
Обогащение API-ответов внешними данными
Скрипт http-response, который обогащает пользовательские данные, вызывая вторичный API:
[Script]
EnrichUsers = type=http-response, script-path=enrich.js, pattern=^https://api\.example\.com/users, requires-body=true, timeout=20
// enrich.js — не более 8 запросов одновременно, это предел для одного скрипта
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()
Модель выполнения
- Все скрипты выполняются на выделенной последовательной очереди для потокобезопасности.
- Каждое выполнение скрипта имеет свой тайм-аут, отсчитываемый с начала выполнения: загрузка исходного кода (удалённый скрипт скачивается в его пределах), выполнение кода и ожидание таймеров и запросов — всё укладывается в него. Если
$done()не вызван в течение тайм-аута, скрипт обрабатывается как пропуск. Скрипт, вернувший управление без$done(), когда не осталось ни незавершённого таймераsetTimeout, ни запроса$httpClient, сразу обрабатывается как пропуск. - Поддерживается пул из 3 предварительно прогретых контекстов; контекст отбрасывается после каждого выполнения и заменяется новым, поэтому глобальные переменные никогда не просачиваются между запусками.
- При запуске движка скриптов Chute запоминает, сколько памяти использует процесс. Когда это значение вырастает более чем на 10 МБ на iOS и tvOS или на 512 МБ на macOS, каждый скрипт пропускается, а его сообщение проходит без изменений; на Android мерой служит используемая куча Java, с запасом 512 МБ. Этот запас рассчитан на весь процесс, а не на один скрипт, и точка отсчёта не берётся заново, пока Chute работает.
- Одновременно может идти не более 16 выполнений скриптов на iOS, tvOS и Android (64 на macOS), и вместе у них может быть не более 4 МБ исходного кода (8 МБ на macOS). Выполнение сверх любого из этих пределов пропускается, а его сообщение проходит без изменений; журнал пишет
execution admission is full. - Исходный код скрипта может занимать не более 1 МБ на macOS и 512 КБ на iOS, tvOS и Android. Сколько скриптов может объявить конфигурация, не ограничено; ограничено то, сколько исходников скриптов может загружаться одновременно, — 32 на macOS и 8 на iOS, tvOS и Android. Сверх этого предела журнал пишет
source load queue is full, и это одно выполнение идёт без исходника, то есть оборачивается пропуском. - Удалённые скрипты (пути HTTP/HTTPS) загружаются при каждом выполнении. Положительное значение
script-update-intervalили ровно-1дополнительно заставляет Chute опрашивать URL условным HEAD-запросом каждые 10 минут. Значение здесь — только переключатель, а не период: опрос идёт раз в 10 минут, какое бы число ни стояло.0(значение по умолчанию) и любое другое отрицательное значение оставляют опрос выключенным.
Скрипты в модулях
Скрипты также можно определять в файлах модулей (.sgmodule) в разделе [Script].
Эта страница — перевод английской версии. При расхождениях приоритет имеет английская версия.