การเขียนสคริปต์ JavaScript

Chute รองรับการเขียนสคริปต์ JavaScript สำหรับการแก้ไขคำขอ/การตอบกลับขั้นสูง การจับคู่กฎที่กำหนดเอง การแปลง DNS และงานตามกำหนดเวลา สคริปต์ทำงานบน JavaScriptCore บนแพลตฟอร์ม Apple และบน QuickJS บน Android และทำตาม API สคริปต์ที่เข้ากันได้กับ Surge

สคริปต์ถูกกำหนดในส่วน [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 ใช่ — เส้นทางไฟล์ในเครื่องหรือ URL HTTP(S) ไปยังสคริปต์ JS
pattern ไม่ (ตรงทุกคำขอ) รูปแบบ regex 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 ไม่ — อาร์กิวเมนต์สตริงที่กำหนดเอง พร้อมใช้งานเป็น $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= และ cronexpr= ซึ่งเป็นการสะกดของ Shadowrocket เป็นคีย์เดียวกัน
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 Control 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


การอ้างอิง API JavaScript

สคริปต์ทำงานในสภาพแวดล้อม JavaScript แบบแซนด์บ็อกซ์ — JavaScriptCore บนแพลตฟอร์ม Apple และ QuickJS บน Android — พร้อมอ็อบเจกต์ส่วนกลางต่อไปนี้

$request (อ่านอย่างเดียว)

พร้อมใช้งานใน: http-request, http-response, http-request-before-send, rule, dns

คุณสมบัติ ประเภท คำอธิบาย
.url String URL คำขอแบบเต็ม พอร์ตจะถูกเขียนไว้เว้นแต่จะเป็นพอร์ตเริ่มต้นของ scheme นั้น และโฮสต์ที่เป็น 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 ID คำขอที่ไม่ซ้ำ
.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-Encoding แบบ gzip หรือ 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})           // ผลการจับคู่กฎ (เฉพาะสคริปต์กฎ)
$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 Rewrite และนโยบาย 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 ที่ส่งคืนมารวมเข้าไป และเนื้อหาเดิมของเซิร์ฟเวอร์จะถูกทิ้ง หากสคริปต์ทำงานที่ส่วนหัวเพียงอย่างเดียว กฎ Body Rewrite และสคริปต์ 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 trailers ของเซิร์ฟเวอร์ก็จะไม่ถูกส่งเช่นกัน 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:// พร้อมตัวเลือกหลัง # ตามปกติ — และจะถูกถามพร้อมกัน คำตอบแรกชนะ หากไม่มีรายการใดใช้ได้เลย การค้นหาจะล้มเหลวแทนที่จะตกไปยังพูลที่ตั้งค่าไว้

ส่วนหัวที่มีหลายบรรทัด

ส่วนหัวหนึ่งอาจปรากฏได้หลายบรรทัด: การตอบกลับที่ตั้งคุกกี้สองตัวจะมีบรรทัด 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 ของคุกกี้มีเครื่องหมายจุลภาค จึงไม่สามารถแยกสตริง Set-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 ตัวเดียวที่รวมด้วย "; " และ 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 สองบรรทัดก็ยังส่งออกไปเป็นสองบรรทัด สตริงอื่นใดจะแทนที่ทุกบรรทัดของส่วนหัวด้วยบรรทัดเดียว หากต้องการเพิ่มคุกกี้ ให้เขียนรายการทั้งหมดเป็นอาร์เรย์: อ่านบรรทัดที่มีอยู่ในโหมด full-header แล้วส่งคืน {"Set-Cookie": [...existing, "c=3"]}

คำขอหนึ่งมีบรรทัด Cookie เพียงบรรทัดเดียว: อาร์เรย์ที่สคริปต์ http-request หรือ http-request-before-send ให้กับ Cookie ใน headers จะถูกรวมด้วย "; " ดังนั้น {"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 แบบ Async

ทำคำขอ 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 ซึ่งเป็นชื่อนโยบายที่กำหนดไว้ในโปรไฟล์ได้ด้วย คำขอนั้นจะถูกส่งออกผ่านนโยบายดังกล่าวแทนเส้นทางเริ่มต้น ชื่อที่โปรไฟล์ไม่ได้กำหนดไว้จะถูกบันทึกเป็นคำเตือน และคำขอจะใช้เส้นทางเริ่มต้น ส่วนรูปแบบ policy-descriptor แบบอินไลน์ของ Surge ไม่รองรับ: จะถูกบันทึกเป็นคำเตือน และคำขอก็จะใช้เส้นทางเริ่มต้นเช่นกัน — ให้กำหนดนโยบายไว้ในโปรไฟล์แล้วส่งชื่อของมันมา

ลายเซ็น Callback: callback(error, response, data)

  • error: สตริงข้อผิดพลาดหรือ null
  • response: {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 คำขอ คำขอที่เกินขีดจำกัดใดขีดจำกัดหนึ่งจะไม่ถูกส่ง และ callback ของมันจะไม่ถูกเรียกเลย บันทึกจะแจ้งเรื่องนี้หนึ่งครั้งต่อรอบการทำงาน เนื้อหาการตอบกลับที่ใหญ่กว่า 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")

อาร์กิวเมนต์ตัวที่สี่ซึ่งไม่บังคับใช้ส่งตัวเลือกของ Surge ได้แก่ url (ลิงก์ที่จะเปิดเมื่อแตะการแจ้งเตือน), action (หากมี url จะถือเป็น open-url และเป็นการกระทำเดียวที่แอปจัดการ — การกระทำอื่น เช่น clipboard ของ Surge จะถูกแนบไปด้วยแต่ไม่มีสิ่งใดจัดการ) และ auto-dismiss (วินาที) ตัวเลือกเหล่านี้จะแนบไปกับการแจ้งเตือน ส่วนคีย์อื่นจะถูกละเว้นพร้อมคำเตือน โดย Chute iOS, Chute Mac และ Chute Android จะจัดการ url และเปิดลิงก์เมื่อคลิกการแจ้งเตือน ส่วน 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 และ tvOS ssid จะว่างเปล่าและ bssid เป็น null ส่วน primaryAddress จะข้ามที่อยู่ของทันเนลเองและที่อยู่แบบลิงก์โลคัล จึงเป็นที่อยู่ที่อุปกรณ์ใช้ออกสู่เครือข่าย และ 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 Rewrite, Header Rewrite, Body Rewrite และการตอบกลับจำลอง — ที่รันไทม์; รับค่าบูลีน

มี $surge ให้ใช้สำหรับสคริปต์ของ Surge โดยมีการเรียกที่ความหมายตรงกันได้แก่ $surge.setSelectGroupPolicy(group, policy) (เหมือน selectPolicy), $surge.selectGroupDetails() (เหมือน selectGroupDetails), $surge.setOutboundMode(mode), $surge.setHTTPCaptureEnabled(enabled), $surge.setRewriteEnabled(enabled) (สวิตช์ของตระกูลการเขียนใหม่ทั้งหมด ได้แก่ URL Rewrite, Header Rewrite, Body Rewrite และการตอบกลับจำลอง) และ $surge.retestGroup(name) ส่วนที่เหลือของ $surge ไม่มีสิ่งที่เทียบเท่าที่นี่ และอ่านค่าได้เป็น undefined ซึ่งสคริปต์ตรวจสอบได้

$httpAPI — สะพานเชื่อม Control API

เรียก HTTP Control 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} ทั้งถูกส่งเข้า callback และถูกส่งคืนกลับมา 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 ของ Chute เท่านั้น และจะเห็นคำขอ 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 encoding จะจบลงพร้อมการเชื่อมต่อ จึงถือว่าครบแล้วและสคริปต์ทำงานตามปกติ ส่วนเนื้อหาที่กำหนดขอบเขตด้วย Content-Length หรือ chunked encoding แต่ถูกการปิดตัดขาดกลางทาง จะไม่ถูกส่งให้สคริปต์ แต่จะถูกส่งให้ไคลเอนต์ตามที่ได้รับมาทุกประการ จากนั้นการเชื่อมต่อจะถูกปิด กฎ Body Rewrite ที่ตรงกับข้อความเดียวกันจะถูกใช้ก่อน และสคริปต์จะเห็นเนื้อหาที่ถูกเขียนใหม่แล้ว

สคริปต์ HTTP Request Before Send

ทำงานก่อนส่งคำขอไปยังอัปสตรีม เมื่อเนื้อหาถูกพักไว้ (สคริปต์นี้มี requires-body=true หรือมีกฎ Body Rewrite หรือสคริปต์ http-request ที่รับเนื้อหาตรงกัน) สคริปต์จะทำงานเมื่อเนื้อหามาถึงครบ คำขอที่ไม่มีเนื้อหา หรือมีเนื้อหาใหญ่กว่า max-size จะไม่ถูกพักไว้ และเนื้อหาที่ใหญ่เกิน max-size ระหว่างถูกพักไว้จะถูกส่งต่อไปตามที่เข้ามา ทั้งสองกรณีสคริปต์จะทำงานตอนส่งส่วนหัวออกไปโดยได้เนื้อหาว่าง และสคริปต์ที่มี requires-body=true จะถูกข้าม มีประโยชน์สำหรับการแก้ไขเนื้อหาคำขอ POST/PUT สคริปต์นี้ทำงานเป็นลำดับสุดท้าย: หลังกฎ Body Rewrite และสคริปต์ 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({}) ภายใน callback — การจบสคริปต์จะยกเลิกคำขอ $httpClient ที่ค้างอยู่ทั้งหมด ดังนั้นการเรียก $done() แบบซิงโครนัสที่ท้ายสคริปต์จะยกเลิกการตรวจสอบสุขภาพก่อนที่การตอบกลับจะมาถึง ด้วยเหตุผลเดียวกัน timeout ของคำขอเองต้องสั้นกว่าของสคริปต์: เมื่อ timeout ของสคริปต์ (ค่าเริ่มต้น 5 วินาที) หมดลงก่อน คำขอจะถูกยกเลิกและ callback ของมันจะไม่ถูกเรียกเลย ตัวอย่างทั้งสองข้างต้นตั้ง 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 — at most 8 requests in flight, the per-script limit
var users = JSON.parse($response.body)
var next = 0
var pending = 0

function launch() {
    while (pending < 8 && next < users.length) {
        fetchAvatar(next++)
    }
    if (pending === 0) {
        $done({body: JSON.stringify(users)})
    }
}

function fetchAvatar(index) {
    pending++
    $httpClient.get("https://internal-api.example.com/avatar/" + users[index].id,
        function(error, resp, data) {
            if (!error && resp.status === 200) {
                users[index].avatar = JSON.parse(data).url
            }
            pending--
            launch()
        }
    )
}

launch()

โมเดลการทำงาน

  • สคริปต์ทั้งหมดทำงานบนคิวอนุกรมเฉพาะเพื่อความปลอดภัยของเธรด
  • การทำงานของสคริปต์แต่ละครั้งมีเวลาหมดต่อสคริปต์ ซึ่งนับตั้งแต่เริ่มการทำงาน: การโหลดซอร์ส (สคริปต์ระยะไกลจะถูกดาวน์โหลดภายในเวลานี้) การรันโค้ด และการรอตัวจับเวลาและคำขอ ล้วนใช้เวลาร่วมกันในนี้ หาก $done() ไม่ถูกเรียกภายในเวลาหมด สคริปต์จะถูกปฏิบัติเป็นส่งผ่าน สคริปต์ที่คืนค่าโดยไม่เรียก $done() ขณะที่ไม่มีตัวจับเวลา setTimeout หรือคำขอ $httpClient ค้างอยู่ จะถูกปฏิบัติเป็นส่งผ่านทันที
  • มีการเก็บพูลคอนเท็กซ์ที่อุ่นเครื่องไว้ล่วงหน้า 3 ตัว; คอนเท็กซ์จะถูกทิ้งหลังการทำงานแต่ละครั้งและแทนที่ด้วยตัวใหม่ ดังนั้นตัวแปรส่วนกลางจะไม่รั่วไหลระหว่างการทำงาน
  • Chute จดการใช้หน่วยความจำของกระบวนการไว้เมื่อเอนจินสคริปต์เริ่มทำงาน เมื่อหน่วยความจำเพิ่มขึ้นเกิน 10 MB บน iOS และ tvOS หรือ 512 MB บน macOS ทุกสคริปต์จะถูกข้ามและข้อความของสคริปต์นั้นจะถูกส่งผ่านไป ส่วนบน Android ตัววัดคือ Java heap ที่ใช้อยู่ โดยมีส่วนเผื่อ 512 MB ส่วนเผื่อนี้เป็นของทั้งกระบวนการ ไม่ใช่ของสคริปต์ตัวใดตัวหนึ่ง และจุดเริ่มต้นจะไม่ถูกวัดใหม่ขณะที่ Chute ยังทำงานอยู่
  • การทำงานของสคริปต์ดำเนินอยู่พร้อมกันได้สูงสุด 16 รายการบน iOS, tvOS และ Android (64 บน macOS) โดยมีซอร์สสคริปต์รวมกันได้ไม่เกิน 4 MB (8 MB บน macOS) การทำงานที่เกินขีดจำกัดใดขีดจำกัดหนึ่งจะถูกข้ามและข้อความของมันจะถูกส่งผ่านไป บันทึกจะแจ้งว่า execution admission is full
  • ซอร์สของสคริปต์หนึ่งรายการมีขนาดได้สูงสุด 1 MB บน macOS และ 512 KB บน 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]

S. Smart Rabbit LLC © All Rights Reserved            updated 2026-09-29 21:57:05

หน้านี้เป็นฉบับแปลจากเวอร์ชันภาษาอังกฤษ หากเนื้อหาไม่ตรงกัน ให้ยึดเวอร์ชันภาษาอังกฤษเป็นหลัก

results matching ""

    No results matching ""