JavaScriptスクリプト
Chuteは、高度なリクエスト/レスポンス変更、カスタムルールマッチング、DNS解決、スケジュールタスクのためのJavaScriptスクリプトをサポートしています。スクリプトはAppleプラットフォームではJavaScriptCore、AndroidではQuickJSで実行され、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 |
はい | — | JSスクリプトへのローカルファイルパスまたはHTTP(S) URL |
pattern |
いいえ | (全てにマッチ) | スクリプトがトリガーされるタイミングをフィルタするURL正規表現パターン。リクエストの完全なURL — 復号後はhttps://…、平文HTTPではhttp://… — のどの位置でもマッチし、パスだけと照合されることはないため、^/apiのようなパターンはマッチしません |
requires-body |
いいえ | 自動検出 | スクリプトに完全なリクエスト/レスポンスボディの受信を強制 |
max-size |
いいえ | 131072 (128KB) | ボディにアクセスするスクリプトの最大ボディサイズ(バイト)。収集されたボディがこのサイズを超えた場合、そのリクエストに対してスクリプトはスキップされます(ボディは切り詰められません)。HTTP/1.x(平文HTTPと復号されたHTTP/1.1)では、この値にかかわらずChuteがスクリプトのためにバッファするのは最大131072バイトなので、そこではこの値は上限を下げることしかできません。復号されたHTTP/2のメッセージは全体が渡され、max-sizeだけで判定されます |
timeout |
いいえ | 5.0秒 | 実行ごとのタイムアウトで、実行の開始時から数えます: ソースの読み込み(リモートスクリプトの場合はダウンロード)、コードの実行、タイマーやリクエストの待機がすべてこの時間を共有します。30秒を超える値は30に切り詰められます |
argument |
いいえ | — | JS内で$argumentとして利用可能なカスタム文字列引数 |
full-header-mode |
いいえ | false | trueのとき、$request.headersと$response.headersはオブジェクトではなく、ヘッダー行1行ごとに1つの{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。名前を指定しない場合は3つすべてで実行されます(Eventスクリプトを参照) |
wake-system |
いいえ | false | 予約済み。パースされますが現在は効果がありません |
enable |
いいえ | true | このスクリプトを有効または無効にする |
script-update-interval |
いいえ | 0 | リモートスクリプトの更新ポーリング(実行モデルを参照) |
binary-body-mode |
いいえ | — | Surgeとの互換性のために受け付け(binary-modeという綴りも可)、プロファイルの保存時にも保持されますが、何も変わりません: 生のバイト列は常に.bodyBytesにあります |
engine |
いいえ | — | 受け付けて保持されますが、無視されます: すべてのスクリプトはプラットフォーム自身のエンジンで実行されます |
スクリプトタイプ
| タイプ文字列 | 列挙型 | 説明 |
|---|---|---|
http-request |
HTTPリクエスト | アップストリームへ送る前にHTTPリクエストをインターセプトして変更 |
http-response |
HTTPレスポンス | クライアントへ返す前にHTTPレスポンスをインターセプトして変更 |
http-request-before-send |
HTTPリクエスト送信前 | アップストリームへ送る直前にリクエストを変更 |
rule |
ルール | カスタムルールマッチングロジック |
dns |
DNS | カスタムDNS解決 |
cron |
Cron | スケジュール/タイマースクリプト |
event |
イベント | システムイベントハンドラ(例: network-changed) |
generic |
汎用 | Surgeとの互換性のために受け付けます。リクエスト、ルール、DNS、イベントのいずれにも結び付かないため、Chuteが自動で実行することはありません。Chute iOSまたはChute Androidのスクリプト一覧の実行か、HTTPコントロールAPIのPOST /api/scripts/runで実行してください |
不明な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環境 — AppleプラットフォームではJavaScriptCore、AndroidではQuickJS — で実行されます。
$request(読み取り専用)
利用可能なタイプ: http-request、http-response、http-request-before-send、rule、dns
| プロパティ | 型 | 説明 |
|---|---|---|
.url |
String | 完全なリクエストURL。ポートはスキームのデフォルトでない限り書かれ、IPv6のホストは角括弧で囲まれます。リクエストターゲットがすでに絶対URLならそのまま使われ、/で始まらないターゲットには/が補われ、ホストがわからない場合はパスだけになります。ruleスクリプトでは宛先ホスト、dnsスクリプトでは問い合わせ中のドメインです |
.method |
String | HTTPメソッド(GET、POSTなど)またはDNSの場合はQUERY |
.headers |
ObjectまたはArray | 名前ごとに値を持つリクエストヘッダー。複数行にわたるヘッダーは、各行の値が1つの文字列に結合されます(複数行にわたるヘッダーを参照)。full-header-mode=trueでは{field, value}の配列です。名前は大文字・小文字を問わずに引けます(ヘッダー名を参照) |
.body |
Stringまたは null | リクエストボディ(UTF-8デコード) |
.bodyBytes |
Bytesまたは null | 生のリクエストボディバイト(下の注意を参照) |
.hostname |
String | ターゲットホスト名 |
.destPort |
Number | 宛先ポート |
.processPath |
String | リクエスト元プロセスパス(macOS。Androidでは、VPNが捕捉するTCP接続についてアプリのAPKパス) |
.userAgent |
String | User-Agentヘッダー値 |
.sourceIP |
String | 送信元IPアドレス |
.listenPort |
Number | プロキシリッスンポート |
.requestId |
String | 一意のリクエストID |
.dnsResult |
String | 解決されたIPアドレス |
.srcPort |
Number | 送信元ポート |
.protocol |
String | 検出されたプロトコル: http、https、tcp、dns |
$response(読み取り専用)
利用可能なタイプ: http-response
| プロパティ | 型 | 説明 |
|---|---|---|
.status |
Number | HTTPステータスコード |
.headers |
ObjectまたはArray | 名前ごとに値を持つレスポンスヘッダー。複数行にわたるヘッダーは、各行の値が1つの文字列に結合されます(複数行にわたるヘッダーを参照)。full-header-mode=trueでは{field, value}の配列です。名前は大文字・小文字を問わずに引けます(ヘッダー名を参照) |
.body |
Stringまたは null | レスポンスボディ(UTF-8デコード) |
.bodyBytes |
Bytesまたは null | 生のレスポンスボディバイト(下の注意を参照) |
.rawBody |
Bytesまたは null | .bodyBytesのエイリアス |
.body、.bodyBytes、.rawBodyが持つのは同じボディで、スクリプトに渡る前にchunkedの分割が取り除かれ、gzipまたはdeflateのContent-Encodingが解かれています。.bodyBytesと.rawBodyはバイトオブジェクトを保持します。これを、バイト列を受け取る呼び出しに渡してください —$utils.ungzip()(その結果もバイトオブジェクトです)や、$done()のbody(生のボディとして受け取ります)です。このオブジェクトの実体はエンジンによって異なります: Appleプラットフォームでは.lengthもインデックスアクセスも無い不透明なラッパー、AndroidではUint8Arrayです。そのため、すべてのプラットフォーム向けのスクリプトは.lengthやインデックスアクセスに頼るべきではありません。内容を読むには、同じバイト列をUTF-8としてデコードした.bodyを使ってください。
$done(value) — 完了ハンドラ
スクリプトの終了時に必ず1回だけ呼び出して完了を通知する必要があります。これを呼ばずに返ったスクリプトは、その場でパススルーとして完了扱いになります。ただしsetTimeoutのタイマーか$httpClientのリクエストが残っている場合は別です。timeout=は実行が始まった瞬間から計られます: ソースの読み込み、コードの実行、その待ち時間のすべてがこれに含まれます。
$done({}) // パススルー — 変更なし
$done() // 接続を中止
$done({matched: true}) // ルールマッチ結果(ルールスクリプトのみ)
$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)。有効な名前は、1文字以上の英字、数字、または
!#$%&'*+-.^_`|~のいずれかからなります。そのため空の名前や、空白、コロン、ASCII以外の文字を含む名前は破棄されます:[JS] Dropped header <name>: not a valid header name
結果の残りはそのまま適用されます。配列形式で渡したヘッダーや、リクエストスクリプトが返すresponseのヘッダーも同じように検査されます。改行、NUL、空白を含むurlは無視され、同じく警告が記録されます。urlのパスとクエリは書かれたとおりに送られ、%20のようなエスケープは解かれません。
HTTPリクエストスクリプトの戻り値:
$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は、headersに何が書かれていてもbodyから計算されます。これはブロック、APIのモック、キャッシュされたコンテンツの返却に便利です。
Chuteが自分で生成する応答 — Map Local、スクリプトの
response、URL書き換えとREJECT系ポリシーの応答およびエラーページ — はHTTPに従います。HEADリクエストへの応答はヘッダーだけで、Content-LengthはGETの場合と同じです。ステータスが204、205、304の応答は、ボディもContent-Lengthも持ちません。
HTTPレスポンススクリプトの戻り値:
$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は元のレスポンスの代わりにそのURLへのリダイレクトを返します。ステータスは返したstatus(例えば301、307、308)で、指定がないか無視された場合は302です。返したheadersは通常どおりマージされ、Locationはurlになります。bodyがなければリダイレクトにボディはなく、サーバーのボディは送られません。HTTP/2ではサーバーのトレーラーも送られません。urlはstatus / headers / bodyの少なくとも1つと一緒に返す必要があります。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://、いつもの#オプション付き)で、同時に問い合わせて最初の応答が採用されます。どのエントリも使えない場合は、設定済みのプールにフォールスルーせずにその解決が失敗します。
複数行にわたるヘッダー
1つのヘッダーが複数行にわたることがあります。例えば2つのCookieを設定するレスポンスにはSet-Cookie行が2行あります。http-request、http-request-before-send、http-responseのスクリプトは、すべての行を読み、複数行を書き出せます。
読み取り
既定では、$request.headersと$response.headersは、ヘッダー名ごとに1つの文字列を対応させたオブジェクトです。ヘッダーが複数行ある場合、各行の値は順に結合されます。Cookieの値は"; "で、それ以外のヘッダー(Set-Cookieを含む)の値は", "でつながります。
$response.headers["Set-Cookie"] // "a=1; Path=/, b=2; Path=/"
$request.headers["Cookie"] // "a=1; b=2"
CookieのExpiresの日付にはカンマが含まれるため、結合されたSet-Cookieの文字列を確実にCookieごとに分け直すことはできません。行ごとに読むには、スクリプトの行にfull-header-mode=trueを加えます。すると$request.headersと$response.headersは、1行につき1つの{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フィールドに分けて送った場合、スクリプトはそれらを"; "で結合した1つのCookieとして読み、set-cookieはそれぞれが1行になります。
書き込み
$done()のheadersは、そこに名前のあるヘッダーだけを変更し、ほかのヘッダーはそのまま残します。リクエストスクリプトが応答として返すresponseのheadersにも同じ規則が適用されます。
| 値 | 効果 |
|---|---|
| 文字列 | そのヘッダーはこの値の1行だけになり、既存の行はすべて置き換えられます |
| 文字列の配列 | 値ごとに1行ずつ、順番どおりにそのヘッダーの行を置き換えます。{"Set-Cookie": ["a=1", "b=2"]}はSet-Cookieを2行送ります |
[] |
そのヘッダーを削除します |
それ以外(数値、null、オブジェクト、文字列以外を含む配列) |
無視され、そのヘッダーはそのまま残ります |
スクリプトが読み取った結合文字列をそのまま返すと、そのヘッダーは元の行を保ちます。したがってconst h = $response.headers; h["X-A"] = "1"; $done({headers: h})はX-Aだけを変え、2行のSet-Cookieは2行のまま送られます。それ以外の文字列は、そのヘッダーのすべての行を1行に置き換えます。Cookieを追加するには、全体を配列で書きます。full-headerモードで既存の行を読み、{"Set-Cookie": [...existing, "c=3"]}を返してください。
リクエストが運ぶCookieは1行です。http-requestまたはhttp-request-before-sendのスクリプトがheadersでCookieに与えた配列は"; "で結合されるので、{"Cookie": ["a=1", "b=2"]}はCookie: a=1; b=2を送ります。
$done()は、full-header-modeの有無にかかわらず、{field, value}オブジェクトの配列としてのheadersも受け付けます。
$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は同じヘッダーなので、各名前は1回だけ書いてください。有効なヘッダー名でない名前は、$doneで説明したとおり破棄されます。 - 改行やNULを含む値は、
$doneで説明したとおり破棄されます。配列ではその値だけが破棄され、すべての値が破棄された場合、そのヘッダーはそのまま残ります。 - HTTP/2では、各行がそれぞれ独立したフィールドになり、名前は小文字になります。
ConnectionやKeep-Aliveのように1つの接続にしか意味のないヘッダーは送られません。
$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())、各ヘッダーは1回だけ、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でないボディは、nullではなく.bodyBytesのようなバイトオブジェクトとして届きます。nullになるのはボディが無いときだけです
$httpClientはhttp-request、http-response、http-request-before-send、cron、eventスクリプトで利用可能です。ruleおよびdnsスクリプトでは利用できません。1つのスクリプトが同時に抱えられる未完了のリクエストは最大8件、すべてのスクリプトを合わせて16件です。どちらかの上限を超えたリクエストは送信されず、そのコールバックも実行されません。ログには実行ごとに一度その旨が記録されます。4 MBを超えるレスポンスボディを受け取ると、そのリクエストは失敗します。
$persistentStore — キーバリューストレージ
スクリプトやプロセスの再起動後も存続する永続的なキーバリューストレージで、すべてのスクリプトが同じキーを読み書きします。Androidでは、Chuteのプライベートストレージに保存され、デバイスのバックアップからは除外されます。
$persistentStore.write(data, key) // 値を保存
$persistentStore.read(key) // 値を取得
$persistentStore.remove(key) // 値を削除
$notification — ローカル通知
ローカルシステム通知を投稿します。Chute Apple TVでは何も表示されません。
$notification.post("Title", "Subtitle", "Notification body text")
省略可能な4番目の引数でSurgeのオプションを渡せます: url(通知をタップしたときに開くリンク)、action(urlがあればopen-urlとみなされます。アプリが処理するアクションはこれだけで、Surgeのclipboardなどほかのアクションは添付されますが、処理するものはありません)、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 AndroidではChuteに対するシステムの通知許可です。これをオフにすると、何も表示されません。Chute Androidはスクリプトの通知を専用の通知チャンネル スクリプト通知 に出し、このチャンネルはシステムの通知設定で個別にオフにできます。通知レポートを参照してください。
$network — ネットワーク情報
読み取り専用のネットワーク状態情報。
$network.dns // DNSサーバーIPの配列
$network.wifi // {ssid: "WiFiName", bssid: null}
$network.v4 // {primaryAddress, primaryRouter}
$network.v6 // {primaryAddress, primaryRouter}
$network.primaryRouter // IPv4のデフォルトゲートウェイ、または null
$network.cellularData // {radio: "LTE" | "5G" | ..., carrier: null}
ssidとbssidが設定されるのは、トンネルがWi-Fiの識別情報を読めるiOSと、ネットワーク名の読み取りにAndroidが求める権限をChuteが持っている場合のAndroidです(Androidクイックスタートを参照)。macOSとtvOSではssidは空、bssidはnullになります。primaryAddressはトンネル自身のアドレスとリンクローカルアドレスを除くため、デバイスがネットワークに出るときのアドレスです。carrierは常にnullです。iOS 16でキャリア名が削除されました。
$environment — ランタイム情報
$environment.system // "iOS"、"macOS" または "Android"
$environment.appVersion // KLNEKit SDKバージョン文字列
$environment.surgeVersion // エンジン自身のバージョン(Surgeスクリプトが読む名前で公開)
$environment.language // BCP 47タグ形式の優先言語(例: "en-US")
$environment.deviceModel // "iPhone"、"Mac"、"AppleTV"のいずれか。Androidでは端末のモデル名(例: "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") // getActiveConnections()が返したidの接続を閉じます
$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()は現在の接続を記述する配列を返します:
[{
id: 1042, // Number — 接続番号。closeConnectionが受け取るもの
host: "example.com", // String — 宛先ホスト(不明の場合は存在しません)
port: 443 // Number — 宛先ポート
}]
残りのメソッドは通常の引数を取り、何も返しません:
selectPolicy(group, policy)—policy(そのグループのpolicyNamesにある名前)をgroupの有効なポリシーにします。未知のグループ名やポリシー名は無視され、ログに警告が記録されます。closeConnection(id)— その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の$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") // 引数1つ: そのパスへの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 // モノトニックタイムスタンプ(システム起動基準からの秒数。エポックではありません)
$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 — イベント情報(イベントスクリプトのみ)
トリガーイベントに関する情報。Chuteはnetwork-changed、engine-started、profile-reloadedを発生させます — Eventスクリプトを参照してください。
console.log("Event: " + $event.name) // "network-changed"
$event.nameは実際に発生したイベントです。event-name=を持つスクリプトはそのイベントでしか実行されないため、名前は常に宣言したものになります。event-name=を持たないスクリプトは3つすべてで実行されるので、どれが発生したかは$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ファイルを読み込んで評価します。ローカルファイルパスのみがサポートされます。http(s)://およびfile:// URLは拒否されます。($scriptはスクリプトメタデータオブジェクト用に予約されています。)
$scriptImport("/path/to/helper.js")
// $persistentStoreを使用してスクリプト間でデータを渡す
スクリプトタイプの詳細
3つのHTTPスクリプトタイプが平文HTTPのリクエストを見るのは、それがChuteのHTTPプロキシを経由して届いた場合だけです。HTTPSのリクエストを見るのは、そのホストが復号される場合だけです — HTTPS復号の
hostnameにホストを追加してください。
HTTPリクエストスクリプト
リクエストヘッダーを受信したときに実行されます。リクエストが転送される前にURL、ヘッダー、ボディを変更できます。
[Script]
ModifyHeaders = type=http-request, script-path=modify.js, pattern=^https://api\.example\.com
HTTPレスポンススクリプト
レスポンスヘッダーを受信したときに実行されます。クライアントに返す前にステータス、ヘッダー、ボディを変更できます。
[Script]
ModifyResponse = type=http-response, script-path=response.js, pattern=^https://api\.example\.com
ボディを受け取るスクリプトは、ボディが全部届いてから実行されます。HTTP/1.xでその前にサーバーが接続を閉じた場合、Content-Lengthもチャンク転送エンコーディングも持たないボディは接続の終わりがボディの終わりなので完全であり、スクリプトは通常どおり実行されます。Content-Lengthまたはチャンク転送エンコーディングで区切られていて、切断によって途中で切れたボディはスクリプトに渡されず、受け取ったとおりのままクライアントへ送られ、その後接続が閉じられます。同じメッセージにマッチしたボディ書き換えルールが先に適用され、スクリプトは書き換え後のボディを受け取ります。
HTTPリクエスト送信前スクリプト
リクエストがアップストリームに送られる直前に実行されます。ボディが保持される場合(このスクリプトに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
ルールスクリプト
カスタムルールマッチング。スクリプトは$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 * * * *
式は、単一のスペースで区切られたちょうど5つのフィールドでなければなりません。それ以外 — 連続したスペース、タブ、4つのフィールド、6つのフィールド — はスクリプトを黙って無効にします: スケジュールに載らず、ログにも何も出ません。
5つのフィールドのうち尊重されるのは分フィールドのみです: */NはN分ごとに実行されます。それ以外の分フィールドは60秒ごとに発火するため、スクリプト自身が現在時刻を確認して動作するかどうかを判断する必要があります。
イベントスクリプト
システムイベントによってトリガーされます。発生するイベントは3つです:
| イベント | 発生するとき |
|---|---|
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はそのイベントでのみスクリプトを実行します。イベントを指定しないスクリプトは3つすべてで実行され、どれが発生したかは$event.nameで判断します。それ以外のイベント(Surgeはnotificationなども発生させます)に結び付けたスクリプトはChuteでは実行されず、設定の読み込み時にその旨がログに出ます。
実践例
モバイルデバイスのリダイレクト
User-Agentに基づいてモバイルユーザーをリダイレクトするhttp-requestスクリプト:
[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レスポンスのコンテンツをブロック
JSON APIレスポンスから広告とスポンサーコンテンツを削除するhttp-responseスクリプト:
[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)})
送信前のリクエストボディを変更
POSTペイロードをサニタイズするhttp-request-before-sendスクリプト:
[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
内部ホスト名をローカルIPに解決するdnsスクリプト:
[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スクリプトはSSIDを読まず、上記のように$httpClientでプローブしてください。
定期的なヘルスチェック
30分ごとにプロキシの健全性をチェックするcronスクリプト:
[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秒)が先に尽きると、リクエストはキャンセルされ、そのコールバックは実行されません。上の2つの例は、どちらも[Script]行でtimeout=15を指定しています。
外部データでAPIレスポンスを強化
二次APIを呼び出してユーザーデータを強化するhttp-responseスクリプト:
[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()が呼び出されない場合、スクリプトはパススルーとして扱われます。setTimeoutのタイマーも$httpClientのリクエストも残っていない状態で$done()を呼ばずに返ったスクリプトは、ただちにパススルーとして扱われます。 - 3つの事前ウォームアップ済みコンテキストのプールが保持されます。コンテキストは実行のたびに破棄され、新しいものと置き換えられるため、グローバル変数が実行間で漏れることはありません。
- Chuteはスクリプトエンジンの起動時に、プロセスのメモリ使用量を記録します。そこからの増加がiOSとtvOSでは 10 MB、macOSでは 512 MB を超えると、すべてのスクリプトがスキップされ、そのメッセージはそのまま通過します。Androidでは使用中のJavaヒープを基準とし、マージンは 512 MB です。このマージンはスクリプト1つごとではなくプロセス全体に対するもので、起点はChuteの動作中に取り直されません。
- 同時に進行できるスクリプト実行は、iOS・tvOS・Androidで最大 16 件(macOSでは 64 件)で、それらのスクリプトソースは合わせて最大 4 MB(macOSでは 8 MB)です。どちらかの上限を超えた実行はスキップされ、そのメッセージはそのまま通過します。ログには
execution admission is fullと出ます。 - スクリプトのソースはmacOSで最大 1 MB、iOS・tvOS・Android で最大 512 KBです。設定が宣言できるスクリプトの数に制限はありません。上限があるのは、同時に読み込み中でいられるスクリプトソースの数で、macOSで 32、iOS・tvOS・Android で 8 です。上限を超えるとログに
source load queue is fullと出て、その1回の実行はソース無しで走ります — つまりパススルーになります。 - リモートスクリプト(HTTP/HTTPSパス)は実行のたびに取得されます。
script-update-intervalが正の値、またはちょうど-1の場合、Chuteはさらに10分ごとに条件付きHEADリクエストでURLをポーリングします。この値はスイッチであって周期ではありません: 数値に何を書いてもポーリングは10分ごとです。0(デフォルト)とそれ以外の負の値では、ポーリングはオフのままです。
モジュールスクリプト統合
スクリプトはモジュールファイル(.sgmodule)の[Script]セクションでも定義できます。
本ページは英語版からの翻訳です。内容に相違がある場合は、英語版が優先されます。