新的 Scripting 功能需要 Surge iOS 4 TF / Surge Mac 3.3.0 版本。
Scripting
脚本声明
[Script]
http-response ^http://www.example.com/test script-path=test.js,max-size=16384,debug=true
cron "* * * * *" script-path=fired.js
policy-group worst script-path=worst.js,debug=true
http-request ^http://httpbin.org script-path=http-request.js,max-size=16384,debug=true,requires-body=true
dns local script-path=dns.js,debug=true
每一行以空格分隔为三个部分,第一部分为脚本类型,第二部分为值,第三部分为参数表。
不同类型的脚本对值的用途不一样,具体参见后文。
参数表包含的参数有:
- script-path:脚本路径,可以是相对路径、绝对路径或者 URL
- script-update-interval:当脚本路径为 URL 时的自动更新频率,单位为秒
- debug:开启 debug 模式,如果是一个本地脚本,那么每次执行脚本时,都会从本地存储重新加载脚本,方便调试。(未来会有更多 debug 功能加入)
- timeout:脚本的最长运行时间,默认为 10s
- requires-body:仅对 http-request 和 http-response 类型生效,表示该脚本需要对 body 进行处理,默认为 false。如果只需要修改 URL 或者 Headers 请不要开启该选项,将大幅节约资源。
- max-size:仅对 http-request 和 http-response 类型生效,表示该脚本最大允许处理的 body 大小,若超过则放弃处理,默认值为 131072 (128KB)。
由于进行脚本修改会需要 Surge 先将 response body 完全下载后再进行处理,如果遇到了较大的数据将导致内存占用量暴增,Surge iOS 受系统内存限制在该情况下极易被直接终止。所以请务必仔细配置 URL 匹配规则,仅对需要的 URL 进行处理。
当返回的数据长度超过 max-size 设定值后,Surge 将放弃对该请求执行脚本并回退到 passthrough 模式。
基本定义
所有脚本允许异步操作,使用 $done(value<Object>) 方法表示完成并返回相应结果。即使是不要求返回结果的脚本类型也应当在完成任务后调用 $done() 退出,否则脚本会因为超时而产生警告。
性能
JS Script 的执行效率极高,不必担心因使用脚本而带来性能问题(Body 修改类除外,会影响整体逻辑),在我们的测试环境下,一个简单脚本的完整执行仅耗时 0.2ms。
公共 API
以下 API 在任何类型脚本中都可以使用,如果有更多需求请在社区提出
当使用参数表时,url 参数必填,其余选填,headers 字段存在将覆盖默认的所有 headers,body 可以为 string 或者 object,为 object 时将自动进行 JSON 编码并设置 Content-Type 为 application/json。
callback 定义为 callback(error<String>, response<Object>, data<String>)
error 为 Null 表示请求成功,response 包含 status 和 headers 两个字段。
其余类似的方法有:$httpClient.get,$httpClient.put,$httpClient.delete,$httpClient.head,$httpClient.options,$httpClient.patch
$notification.post(title<String>, subtitle<String>, body<String>)
向通知中心发送通知,Surge iOS 上需开启通知总开关。
$utils.geoip(ip<String>)
进行 GeoIP 查询,返回结果为 ISO 3166 的国家编码
$surge.setSelectGroupPolicy(groupName<String>, policyName<String>)
修改 select 策略组的当前选项,返回 bool 值表示是否成功
$surge.selectGroupDetails()
获得当前 select 策略组的信息,包含组名称,子策略,和当前选择的策略。
$surge.setOutboundMode(mode<String>)
mode 取值可为 "direct", "global-proxy", "rule",修改 Surge 的 Outbound Mode,返回 bool 值表示是否成功
$surge.setHTTPCaptureEnabled(enabled<Boolean>)
$surge.setCellularModeEnabled(enabled<Boolean>)
$surge.setRewriteEnabled(enabled<Boolean>)
$surge.setEnhancedModeEnabled(enabled<Boolean>) Surge Mac Only
用于控制 Surge 的各项功能的开启
$network
当前网络状态的总览,包含 IP 和 SSID 等信息
$script.name<String> 当前执行的脚本的文件名
$script.startTime<Date> 当前执行的脚本的开始时间
$persistentStore.write(data<String>, [key<String>]) 持久化保存数据,返回 bool 值表示是否成功,仅支持传入 string。
$persistentStore.read([key<String>]) 读取保存的持久化数据,返回 string 或 Null。
不传入 key 时,同一个 script-path 的脚本共享一个存储池。可传入一个固定的 key 以在多个脚本间共享数据。
脚本类型
http-request
用于修改 HTTP 请求体,该类型下第二参数为匹配 URL 的正则表达式,被匹配到的请求会被执行脚本。
传入参数为 $request,字段为
- $request.url<String>:请求的 URL
- $request.body<String>:请求的 Body,以 UTF-8 解码后的字符串,仅当 requires-body = true 时有效
- $request.rawBody<Uint8Array>:请求的原始二进制 Body,仅当 requires-body = true 时有效,请注意 JS 的 Uint8Array 没有只读限制,但是请勿修改该对象,如果要修改应在复制后修改再返回。
- $request.method<String>:请求的 HTTP 方法
- $request.headers<Object>:请求的 Header
应执行 $done 返回一个对象,可选包含字段:
- url<String>:使用该 URL 覆盖原请求的 URL,注意使用脚本修改 URL 时不会像 URL Rewrite 那样自动调整 Header 的 Host 字段,如果需要调整需要手动返回新的 headers 字段
- headers<Object>:使用该 headers 词典完全覆盖原来的 headers,注意部分 HTTP 特殊字段不可被修改,如 Content-Length
- body<String>:使用该 body 覆盖原来的请求 body,仅当 requires-body = true 时有效。
- rawBody<Uint8Array>:使用该 raw body 覆盖原来的请求 body,仅当 requires-body = true 时有效。该属性优先级高于 body。
- response<Object>:如果返回了该字段,则表示应直接返回该结果而不进行实际的网络请求,仅当 requires-body = false 时有效。可包含三个字段:
- $status<Number>: 响应的 HTTP 状态码。
- $headers<Object>: 响应的 HTTP Headers。
- $body<String>: 响应的 HTTP Body,将以 UTF-8 编码后返回。
使用 $done(); 表示终止该请求,使用 $done({}); 表示不对该请求进行修改。
当前版本的一些限制:
- 当请求使用 chunked encoding 上传 body 时,不可以进行 body 修改。
- 当 Expect: 100-continue 存在时,不可以进行 body 修改。
一个简单样例
let headers = $request.headers;
headers['X-Modified-By'] = 'Surge';
$done({headers});
注意样例使用了 JS ES6 语法。
http-response
用于修改 HTTP 返回体,该类型下第二参数为匹配 URL 的正则表达式,被匹配到的请求会被执行脚本。
传入参数为 $request 和 $response,字段为
$request.url<String>:请求的 URL
$request.method<String>:请求的 HTTP 方法
$response.status<Number>: 响应的 HTTP 状态码
$response.headers<Object>: 响应的 HTTP Headers
$response.body<String>: 响应的 HTTP Body,以 UTF-8 解码后的字符串,仅当 requires-body = true 时有效
$request.rawBody<Uint8Array>:响应的原始二进制 Body,仅当 requires-body = true 时有效,请注意 JS 的 Uint8Array 没有只读限制,但是请勿修改该对象,如果要修改应在复制后修改再返回。
应执行 $done 返回一个对象,可选包含三个字段:
- body<String>:使用该 body 覆盖原来的响应 body,仅当 requires-body = true 时有效。
- rawBody<Uint8Array>:使用该 raw body 覆盖原来的请求 body,仅当 requires-body = true 时有效。该属性优先级高于 body。
- headers<Object>:使用该 headers 词典完全覆盖原来的 headers,注意部分 HTTP 特殊字段不可被修改,如 Content-Length
- status<Number>:覆盖原来的 HTTP 状态码
使用 $done(); 表示终止该请求,使用 $done({}); 表示不对该请求进行修改。
一个简单样例
let headers = $response.headers;
headers['X-Modified-By'] = 'Surge';
$done({headers});
注意样例使用了 JS ES6 语法。
cron
可配置 Surge 在特定的时间执行脚本,触发时间配置使用 crontab 的样式。该类型下第二参数为 crontab 表达式,常见的 crontab 为五位表示,即 * * * * * 表示每分钟执行一次,Surge 兼容五位表示和六位表示,可用 * * * * * * 表示每秒钟执行一次。但不支持 @daily 这样的别名。
crontab 表达式撰写方法请参见相关文档,一些样例如下:
- at 2am daily: 0 2 * * *
- at 5 AM and 5 PM daily: 0 5,17 * * *
- on every minutes: * * * * *
- on every Sunday at 5 PM: 0 17 * * sun
- every 10 minutes: */10 * * * *
脚本任务执行完毕后请调用 $done() 退出
一个简单样例
// cron "0 2 * * *" script-path=cron.js
$surge.setSelectGroupPolicy('Group', 'Proxy');
$done();
event
在发生特定事件时执行脚本,该类型下第二参数为事件名称,目前只有 network-changed 一个事件。
脚本任务执行完毕后请调用 $done() 退出
一个简单样例
// event network-changed script-path=network-changed.js
$notification.post('DNS Update', $network.dns.join(', '));
$done();
rule
使用脚本进行规则判定,该类型下第二参数为规则名
rule ssid-rule script-path=ssid-rule.js
然后在 [Rule] 中加入规则
SCRIPT,ssid-rule,DIRECT
脚本返回一个词典,字段 matched<Boolean> 表示是否匹配该规则。
传入参数有:
- $request.hostname<String>
- $request.destPort<Number>
- $request.processPath<String>
- $request.userAgent<String>
- $request.url<String>
- $request.sourceIP<String>
- $request.listenPort<Number>
- $request.dnsResult<Object>
默认情况下,SCRIPT 规则不会触发 DNS 解析,如果需要进行 DNS 解析,可使用 requires-resolve 修饰规则
SCRIPT,ssid-rule,DIRECT,requires-resolve
DNS 结果将出现在 $request.dnsResult 字段。
一个简单样例
var hostnameMatched = ($request.hostname === 'home.com');
var ssidMatched = ($network.wifi.ssid === 'My Home');
$done({matched: (hostnameMatched && ssidMatched)});
policy-group
使用脚本去决定 policy-group,该类型下第二参数为脚本名。
policy-group jsgroup script-path=dnspod.js
对应策略组定义如下:
[Policy Group]
sgroup = script, policyA, policyB, policyC
传入参数为 $policyNames<Array>,指定的可用策略名数组。
返回结果为 {selected: "policy-name"},选定的策略名称。
dns
使用脚本去执行 DNS 解析操作,该类型下第二参数为脚本名。
dns dnspod script-path=dnspod.js
之后需要在 [Host] 中对相应域名进行配置。
[Host]
baidu.com = script:dnspod
*.baidu.com = script:dnspod
传入参数为 $domain,当前查询的域名。
返回结果可为 address<String>,addresses<Array>,server<String>,servers<Array> 中的任意一个
- 当返回 address<String> 或者 addresses<Array> 时,直接使用该 IP 地址为结果,必须是一个有效的 IPv4 或者 IPv6 地址的字符串表示。使用 addresses 返回多个结果的数组。
- 当返回 server<String> 或者 servers<Array> 时,表示应交给特定的 DNS 服务器进行查询。
当返回 address<String> 或者 addresses<Array> 时,可以额外返回 ttl 字段,将本次的结果记入 DNS 缓存避免重复查询,单位为秒。
以下样例使用了 DNSPod 的公开 HTTP DNS API,通过脚本扩展的方式让 Surge 可以随意支持各种协议的 DNS 查询
$httpClient.get('http://119.29.29.29/d?dn=' + $domain, function(error, response, data){
if (error) {
$done({}); // Fallback to standard DND query
} else {
$done({addresses: data.split(';'), ttl: 600});
}
});