Skip to content

Commit e6d4fca

Browse files
committed
feat: Beta 版支持 Surge/Loon Body Rewrite
1 parent f386191 commit e6d4fca

File tree

2 files changed

+221
-1
lines changed

2 files changed

+221
-1
lines changed

Rewrite-Parser.beta.js

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ let hostBox = [] //host
235235
let ruleBox = [] //规则
236236
let rwBox = [] //重写
237237
let rwhdBox = [] //HeaderRewrite
238+
let rwbodyBox = [] // Body Rewrite
238239
let panelBox = [] //Panel信息
239240
let jsBox = [] //脚本
240241
let mockBox = [] //MapLocal或echo-response
@@ -260,6 +261,7 @@ let host = []
260261
let rules = []
261262
let URLRewrite = []
262263
let HeaderRewrite = []
264+
let BodyRewrite = []
263265
let MapLocal = []
264266
let script = []
265267
let cron = []
@@ -314,6 +316,16 @@ if (binaryInfo != null && binaryInfo.length > 0) {
314316
eval(evJsori)
315317
eval(evUrlori)
316318

319+
// [Body Rewrite] 部分 rwbodyBox
320+
let bodyRewrite = body.match(/(^|\n)\[Body Rewrite\]\n([\s\S]*?)\s*(\n\[|$)/)?.[2]
321+
322+
if (bodyRewrite) {
323+
for await (let [y, x] of bodyRewrite.match(/[^\r\n]+/g).entries()) {
324+
const [_, type, regex, value] = x.match(/^(http-request|http-response)\s+?(.*?)\s+?(.*?)$/)
325+
rwbodyBox.push({ type, regex, value })
326+
}
327+
}
328+
317329
body = body.match(/[^\r\n]+/g)
318330

319331
for await (let [y, x] of body.entries()) {
@@ -467,6 +479,76 @@ if (binaryInfo != null && binaryInfo.length > 0) {
467479
rw_redirect(x, mark)
468480
}
469481

482+
// Loon body rewrite 解析 不用这个 因为需要合并到一个脚本中对一个请求/响应进行多个操作
483+
// if (/\s((request|response)-body-replace-regex)\s/.test(x)) {
484+
// let [_, regex, __, type, suffix] = x.match(/^(.*?)\s+?((request|response)-body-replace-regex)\s+?(.*?)\s*$/)
485+
// type = `http-${type}`
486+
// const suffixArray = suffix.split(/\s+/)
487+
// const newSuffixArray = []
488+
// for (let index = 0; index < suffixArray.length; index += 2) {
489+
// const key = suffixArray[index]
490+
// const value = suffixArray[index + 1]
491+
492+
// if (value != null) {
493+
// newSuffixArray.push(
494+
// `${/\\x20/.test(key) ? `"${key.replace(/\\x20/g, ' ')}"` : key} ${
495+
// /\\x20/.test(value) ? `"${value.replace(/\\x20/g, ' ')}"` : value
496+
// }`
497+
// )
498+
// }
499+
// }
500+
501+
// rwbodyBox.push({ type, regex, value: newSuffixArray.join(' ') })
502+
// }
503+
504+
if (/\s((request|response)-body-(json-(add|del|replace)|replace-regex))\s/.test(x)) {
505+
let [_, regex, __, httpType, action, ___, suffix] = x.match(
506+
/^(.*?)\s+?((request|response)-body-(json-(add|del|replace)|replace-regex))\s+?(.*?)\s*$/
507+
)
508+
const suffixArray = suffix.split(/\s+/)
509+
let newSuffixArray = []
510+
if (action === 'json-del') {
511+
if (suffix) {
512+
newSuffixArray = suffixArray.map(item => (/\\x20/.test(item) ? `${item.replace(/\\x20/g, ' ')}` : item))
513+
}
514+
} else {
515+
for (let index = 0; index < suffixArray.length; index += 2) {
516+
const key = suffixArray[index]
517+
const value = suffixArray[index + 1]
518+
519+
if (value != null) {
520+
newSuffixArray.push([
521+
/\\x20/.test(key) ? `${key.replace(/\\x20/g, ' ')}` : key,
522+
/\\x20/.test(value) ? `${value.replace(/\\x20/g, ' ')}` : value,
523+
])
524+
}
525+
}
526+
}
527+
const jsurl = 'https://raw.githubusercontent.com/Script-Hub-Org/Script-Hub/main/scripts/body-rewrite.js'
528+
const jstype = `http-${httpType}`
529+
const jsptn = regex
530+
let args = [[action, newSuffixArray]]
531+
532+
const index = jsBox.findIndex(i => i.jsurl === jsurl && i.jstype === jstype && i.jsptn === jsptn)
533+
if (index === -1) {
534+
jsBox.push({
535+
jsname: `body_rewrite_${y}`,
536+
jstype,
537+
jsptn,
538+
jsurl,
539+
rebody: true,
540+
size: -1,
541+
timeout: '30',
542+
jsarg: encodeURIComponent(JSON.stringify(args)),
543+
ori: x,
544+
num: y,
545+
})
546+
} else {
547+
let jsargs = JSON.parse(decodeURIComponent(jsBox[index].jsarg))
548+
jsBox[index].jsarg = encodeURIComponent(JSON.stringify([...jsargs, args[0]]))
549+
}
550+
}
551+
470552
//header rewrite 解析
471553
if (/\s(response-)?header-(?:del|add|replace|replace-regex)\s/.test(x)) {
472554
mark = getMark(y, body)
@@ -814,6 +896,9 @@ if (binaryInfo != null && binaryInfo.length > 0) {
814896
return curr
815897
}, [])
816898

899+
// BodyRewrite 需不要去重 会顺序执行
900+
rwbodyBox = [...new Set(rwbodyBox)]
901+
817902
panelBox = panelBox.reduce((curr, next) => {
818903
/*判断对象中是否已经有该属性 没有的话 push 到 curr数组*/
819904
obj[next.scriptname] ? '' : (obj[next.scriptname] = curr.push(next))
@@ -1051,6 +1136,11 @@ if (binaryInfo != null && binaryInfo.length > 0) {
10511136
} //switch
10521137
} //reject redirect输出for
10531138

1139+
for (let i = 0; i < rwbodyBox.length; i++) {
1140+
const { type, regex, value } = rwbodyBox[i]
1141+
BodyRewrite.push(`${type} ${regex} ${value}`)
1142+
}
1143+
10541144
//headerRewrite输出
10551145
for (let i = 0; i < rwhdBox.length; i++) {
10561146
noteK = rwhdBox[i].noteK ? '#' : ''
@@ -1460,6 +1550,8 @@ if (binaryInfo != null && binaryInfo.length > 0) {
14601550

14611551
HeaderRewrite = (HeaderRewrite[0] || '') && `[Header Rewrite]\n${HeaderRewrite.join('\n')}`
14621552

1553+
BodyRewrite = (BodyRewrite[0] || '') && `[Body Rewrite]\n${BodyRewrite.join('\n')}`
1554+
14631555
MapLocal = (MapLocal[0] || '') && `[Map Local]\n${MapLocal.join('\n\n')}`
14641556

14651557
host = (host[0] || '') && `[Host]\n${host.join('\n')}`
@@ -1492,6 +1584,8 @@ ${URLRewrite}
14921584
14931585
${HeaderRewrite}
14941586
1587+
${BodyRewrite}
1588+
14951589
${MapLocal}
14961590
14971591
${Panel}
@@ -1895,7 +1989,6 @@ function getMockInfo(x, mark, y) {
18951989
if (oritype === 'base64') {
18961990
mockbase64 = true
18971991
}
1898-
console.log({ mockbase64 })
18991992
}
19001993
switch (targetApp) {
19011994
case 'surge-module':

scripts/body-rewrite.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
const NAME = 'body-rewrite'
2+
const TITLE = 'body-rewrite'
3+
const $ = new Env(NAME)
4+
// $argument =
5+
// '%5B%5B%22json-del%22%2C%5B%22a.b%5B1%5D%22%2C%22a.b%5B2%5D%22%5D%5D%2C%5B%22replace-regex%22%2C%5B%5B%22a%7Cb%7Cc%22%2C%22req%22%5D%5D%5D%5D'
6+
let result = {}
7+
!(async () => {
8+
if (typeof $argument == 'undefined') throw new Error('$argument 不能为空')
9+
let actions
10+
if (typeof $argument != 'undefined') {
11+
if (!$argument) throw new Error('$argument 不能为空')
12+
try {
13+
actions = JSON.parse(decodeURIComponent($argument))
14+
} catch (e) {
15+
throw new Error('$argument 解析失败')
16+
}
17+
$.log(JSON.stringify(actions, null, 2))
18+
}
19+
let shouldParseJSON = actions.find(([action]) => action.startsWith('json'))
20+
$.log(`shouldParseJSON: ${shouldParseJSON ? 'true' : 'false'}`)
21+
22+
let body
23+
if (typeof $response != 'undefined') {
24+
body = $response.body
25+
} else if (typeof $request != 'undefined') {
26+
body = $request.body
27+
} else {
28+
throw new Error('不为 请求 或 响应')
29+
// body = JSON.stringify({
30+
// a: {
31+
// b: ['c', 'd', 'f', 'g'],
32+
// },
33+
// })
34+
}
35+
// $.log($.toStr(body))
36+
if (shouldParseJSON) {
37+
try {
38+
body = JSON.parse(body)
39+
} catch (e) {
40+
throw new Error('JSON 解析失败')
41+
}
42+
}
43+
44+
for (let [action, items] of actions) {
45+
if (action === 'json-del') {
46+
for (let item of items) {
47+
unset(body, item)
48+
}
49+
} else if (action === 'json-add') {
50+
for (let item of items) {
51+
$.lodash_set(body, item[0], item[1])
52+
}
53+
} else if (action === 'json-replace') {
54+
for (let item of items) {
55+
if ($.lodash_get(body, item[0]) != null) {
56+
$.lodash_set(body, item[0], item[1])
57+
}
58+
}
59+
} else if (action === 'replace-regex') {
60+
if (shouldParseJSON) {
61+
body = JSON.stringify(body)
62+
}
63+
for (let item of items) {
64+
body = body.replace(new RegExp(item[0], 'g'), item[1])
65+
}
66+
try {
67+
body = JSON.parse(body)
68+
} catch (e) {
69+
throw new Error('replace-regex 过程中 JSON 解析失败')
70+
}
71+
}
72+
}
73+
74+
result = {
75+
body: JSON.stringify(body),
76+
}
77+
})()
78+
.catch(async e => {
79+
$.logErr(e)
80+
$.logErr($.toStr(e))
81+
await notify(TITLE, '❌', `${$.lodash_get(e, 'message') || $.lodash_get(e, 'error') || e}`)
82+
})
83+
.finally(async () => {
84+
// $.log($.toStr(result))
85+
$.done(result)
86+
})
87+
function unset(object, path) {
88+
if (object == null) {
89+
return true
90+
}
91+
92+
const paths = path.replace(/\[(\d+)\]/g, '.$1').split('.')
93+
94+
var current = object
95+
for (var i = 0; i < paths.length - 1; i++) {
96+
var key = paths[i]
97+
if (!(key in current)) {
98+
return true
99+
}
100+
current = current[key]
101+
if (current == null) {
102+
return true
103+
}
104+
}
105+
106+
var finalKey = paths[paths.length - 1]
107+
108+
// 处理数组的情况
109+
if (Array.isArray(current)) {
110+
var index = parseInt(finalKey, 10)
111+
if (!isNaN(index)) {
112+
current.splice(index, 1)
113+
return true
114+
}
115+
}
116+
117+
// 处理对象的属性删除
118+
delete current[finalKey]
119+
return true
120+
}
121+
// 通知
122+
async function notify(title, subt, desc, opts) {
123+
$.msg(title, subt, desc, opts)
124+
}
125+
126+
// prettier-ignore
127+
function Env(t,e){class s{constructor(t){this.env=t}send(t,e="GET"){t="string"==typeof t?{url:t}:t;let s=this.get;return"POST"===e&&(s=this.post),new Promise(((e,r)=>{s.call(this,t,((t,s,a)=>{t?r(t):e(s)}))}))}get(t){return this.send.call(this.env,t)}post(t){return this.send.call(this.env,t,"POST")}}return new class{constructor(t,e){this.name=t,this.http=new s(this),this.data=null,this.dataFile="box.dat",this.logs=[],this.isMute=!1,this.isNeedRewrite=!1,this.logSeparator="\n",this.encoding="utf-8",Object.assign(this,e)}getEnv(){return"undefined"!=typeof $environment&&$environment["surge-version"]?"Surge":"undefined"!=typeof $environment&&$environment["stash-version"]?"Stash":"undefined"!=typeof module&&module.exports?"Node.js":"undefined"!=typeof $task?"Quantumult X":"undefined"!=typeof $loon?"Loon":"undefined"!=typeof $rocket?"Shadowrocket":void 0}isNode(){return"Node.js"===this.getEnv()}isQuanX(){return"Quantumult X"===this.getEnv()}isSurge(){return"Surge"===this.getEnv()}isLoon(){return"Loon"===this.getEnv()}isShadowrocket(){return"Shadowrocket"===this.getEnv()}isStash(){return"Stash"===this.getEnv()}toObj(t,e=null){try{return JSON.parse(t)}catch{return e}}toStr(t,e=null){try{return JSON.stringify(t)}catch{return e}}getjson(t,e){let s=e;const r=this.getdata(t);if(r)try{s=JSON.parse(this.getdata(t))}catch{}return s}setjson(t,e){try{return this.setdata(JSON.stringify(t),e)}catch{return!1}}getScript(t){return new Promise((e=>{this.get({url:t},((t,s,r)=>e(r)))}))}runScript(t,e){return new Promise((s=>{let r=this.getdata("@chavy_boxjs_userCfgs.httpapi");r=r?r.replace(/\n/g,"").trim():r;let a=this.getdata("@chavy_boxjs_userCfgs.httpapi_timeout");a=a?1*a:20,a=e&&e.timeout?e.timeout:a;const[o,i]=r.split("@"),n={url:`http://${i}/v1/scripting/evaluate`,body:{script_text:t,mock_type:"cron",timeout:a},headers:{"X-Key":o,Accept:"*/*"},timeout:a};this.post(n,((t,e,r)=>s(r)))})).catch((t=>this.logErr(t)))}loaddata(){if(!this.isNode())return{};{this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),r=!s&&this.fs.existsSync(e);if(!s&&!r)return{};{const r=s?t:e;try{return JSON.parse(this.fs.readFileSync(r))}catch(t){return{}}}}}writedata(){if(this.isNode()){this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),r=!s&&this.fs.existsSync(e),a=JSON.stringify(this.data);s?this.fs.writeFileSync(t,a):r?this.fs.writeFileSync(e,a):this.fs.writeFileSync(t,a)}}lodash_get(t,e,s){const r=e.replace(/\[(\d+)\]/g,".$1").split(".");let a=t;for(const t of r)if(a=Object(a)[t],void 0===a)return s;return a}lodash_set(t,e,s){return Object(t)!==t?t:(Array.isArray(e)||(e=e.toString().match(/[^.[\]]+/g)||[]),e.slice(0,-1).reduce(((t,s,r)=>Object(t[s])===t[s]?t[s]:t[s]=Math.abs(e[r+1])>>0==+e[r+1]?[]:{}),t)[e[e.length-1]]=s,t)}getdata(t){let e=this.getval(t);if(/^@/.test(t)){const[,s,r]=/^@(.*?)\.(.*?)$/.exec(t),a=s?this.getval(s):"";if(a)try{const t=JSON.parse(a);e=t?this.lodash_get(t,r,""):e}catch(t){e=""}}return e}setdata(t,e){let s=!1;if(/^@/.test(e)){const[,r,a]=/^@(.*?)\.(.*?)$/.exec(e),o=this.getval(r),i=r?"null"===o?null:o||"{}":"{}";try{const e=JSON.parse(i);this.lodash_set(e,a,t),s=this.setval(JSON.stringify(e),r)}catch(e){const o={};this.lodash_set(o,a,t),s=this.setval(JSON.stringify(o),r)}}else s=this.setval(t,e);return s}getval(t){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.read(t);case"Quantumult X":return $prefs.valueForKey(t);case"Node.js":return this.data=this.loaddata(),this.data[t];default:return this.data&&this.data[t]||null}}setval(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.write(t,e);case"Quantumult X":return $prefs.setValueForKey(t,e);case"Node.js":return this.data=this.loaddata(),this.data[e]=t,this.writedata(),!0;default:return this.data&&this.data[e]||null}}initGotEnv(t){this.got=this.got?this.got:require("got"),this.cktough=this.cktough?this.cktough:require("tough-cookie"),this.ckjar=this.ckjar?this.ckjar:new this.cktough.CookieJar,t&&(t.headers=t.headers?t.headers:{},void 0===t.headers.Cookie&&void 0===t.cookieJar&&(t.cookieJar=this.ckjar))}get(t,e=(()=>{})){switch(t.headers&&(delete t.headers["Content-Type"],delete t.headers["Content-Length"],delete t.headers["content-type"],delete t.headers["content-length"]),t.params&&(t.url+="?"+this.queryStr(t.params)),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient.get(t,((t,s,r)=>{!t&&s&&(s.body=r,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,r)}));break;case"Quantumult X":this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then((t=>{const{statusCode:s,statusCode:r,headers:a,body:o,bodyBytes:i}=t;e(null,{status:s,statusCode:r,headers:a,body:o,bodyBytes:i},o,i)}),(t=>e(t&&t.error||"UndefinedError")));break;case"Node.js":let s=require("iconv-lite");this.initGotEnv(t),this.got(t).on("redirect",((t,e)=>{try{if(t.headers["set-cookie"]){const s=t.headers["set-cookie"].map(this.cktough.Cookie.parse).toString();s&&this.ckjar.setCookieSync(s,null),e.cookieJar=this.ckjar}}catch(t){this.logErr(t)}})).then((t=>{const{statusCode:r,statusCode:a,headers:o,rawBody:i}=t,n=s.decode(i,this.encoding);e(null,{status:r,statusCode:a,headers:o,rawBody:i,body:n},n)}),(t=>{const{message:r,response:a}=t;e(r,a,a&&s.decode(a.rawBody,this.encoding))}))}}post(t,e=(()=>{})){const s=t.method?t.method.toLocaleLowerCase():"post";switch(t.body&&t.headers&&!t.headers["Content-Type"]&&!t.headers["content-type"]&&(t.headers["content-type"]="application/x-www-form-urlencoded"),t.headers&&(delete t.headers["Content-Length"],delete t.headers["content-length"]),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient[s](t,((t,s,r)=>{!t&&s&&(s.body=r,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,r)}));break;case"Quantumult X":;t.method=s,this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then((t=>{const{statusCode:s,statusCode:r,headers:a,body:o,bodyBytes:i}=t;e(null,{status:s,statusCode:r,headers:a,body:o,bodyBytes:i},o,i)}),(t=>e(t&&t.error||"UndefinedError")));break;case"Node.js":let r=require("iconv-lite");this.initGotEnv(t);const{url:a,...o}=t;this.got[s](a,o).then((t=>{const{statusCode:s,statusCode:a,headers:o,rawBody:i}=t,n=r.decode(i,this.encoding);e(null,{status:s,statusCode:a,headers:o,rawBody:i,body:n},n)}),(t=>{const{message:s,response:a}=t;e(s,a,a&&r.decode(a.rawBody,this.encoding))}))}}time(t,e=null){const s=e?new Date(e):new Date;let r={"M+":s.getMonth()+1,"d+":s.getDate(),"H+":s.getHours(),"m+":s.getMinutes(),"s+":s.getSeconds(),"q+":Math.floor((s.getMonth()+3)/3),S:s.getMilliseconds()};/(y+)/.test(t)&&(t=t.replace(RegExp.$1,(s.getFullYear()+"").substr(4-RegExp.$1.length)));for(let e in r)new RegExp("("+e+")").test(t)&&(t=t.replace(RegExp.$1,1==RegExp.$1.length?r[e]:("00"+r[e]).substr((""+r[e]).length)));return t}queryStr(t){let e="";for(const s in t){let r=t[s];null!=r&&""!==r&&("object"==typeof r&&(r=JSON.stringify(r)),e+=`${s}=${r}&`)}return e=e.substring(0,e.length-1),e}msg(e=t,s="",r="",a){const o=t=>{switch(typeof t){case void 0:return t;case"string":switch(this.getEnv()){case"Surge":case"Stash":default:return{url:t};case"Loon":case"Shadowrocket":return t;case"Quantumult X":return{"open-url":t};case"Node.js":return}case"object":switch(this.getEnv()){case"Surge":case"Stash":case"Shadowrocket":default:{let e=t.url||t.openUrl||t["open-url"];return{url:e}}case"Loon":{let e=t.openUrl||t.url||t["open-url"],s=t.mediaUrl||t["media-url"];return{openUrl:e,mediaUrl:s}}case"Quantumult X":{let e=t["open-url"]||t.url||t.openUrl,s=t["media-url"]||t.mediaUrl,r=t["update-pasteboard"]||t.updatePasteboard;return{"open-url":e,"media-url":s,"update-pasteboard":r}}case"Node.js":return}default:return}};if(!this.isMute)switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:$notification.post(e,s,r,o(a));break;case"Quantumult X":$notify(e,s,r,o(a));break;case"Node.js":}if(!this.isMuteLog){let t=["","==============📣系统通知📣=============="];t.push(e),s&&t.push(s),r&&t.push(r),console.log(t.join("\n")),this.logs=this.logs.concat(t)}}log(...t){t.length>0&&(this.logs=[...this.logs,...t]),console.log(t.join(this.logSeparator))}logErr(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:this.log("",`❗️${this.name}, 错误!`,t);break;case"Node.js":this.log("",`❗️${this.name}, 错误!`,t.stack)}}wait(t){return new Promise((e=>setTimeout(e,t)))}done(t={}){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:$done(t);break;case"Node.js":process.exit(1)}}}(t,e)}

0 commit comments

Comments
 (0)