การเขียนสคริปต์ 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: สตริงข้อผิดพลาดหรือnullresponse:{status: Number, headers: Object}หรือnullheadersหาส่วนหัวได้ไม่ว่าชื่อจะเขียนด้วยตัวพิมพ์แบบใด และแจกแจงชื่อแบบเดียวกับ$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 และ tvOSssidจะว่างเปล่าและ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()— ล้างแคช DNSstartURLTest(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]
หน้านี้เป็นฉบับแปลจากเวอร์ชันภาษาอังกฤษ หากเนื้อหาไม่ตรงกัน ให้ยึดเวอร์ชันภาษาอังกฤษเป็นหลัก