diff --git a/NobyDa_BoxJs.json b/NobyDa_BoxJs.json index 086ebda..26c427c 100644 --- a/NobyDa_BoxJs.json +++ b/NobyDa_BoxJs.json @@ -664,6 +664,32 @@ "https://raw.githubusercontent.com/NobyDa/mini/master/Alpha/ctrip.png", "https://raw.githubusercontent.com/NobyDa/mini/master/Color/ctrip.png" ] + }, + { + "id": "GoogleCAPTCHA", + "name": "Google搜索人机验证", + "keys": [ + "GOOGLE_CAPTCHA" + ], + "descs_html": [ + "

脚本说明以及配置请查看脚本注释

" + ], + "settings": [ + { + "id": "@GOOGLE_CAPTCHA.Regex", + "name": "代理策略/策略组正则表达式", + "val": "", + "type": "text", + "placeholder": "^(🇸🇬|🇭🇰)\\s.*\\d+$", + "desc": "筛选的代理策略/策略组, 限制20个, 留空则表示随机使用。" + } + ], + "author": "@NobyDa", + "repo": "https://github.com/NobyDa/Script/blob/master/Surge/JS/Google_CAPTCHA.js", + "icons": [ + "https://raw.githubusercontent.com/NobyDa/mini/master/Alpha/Google.png", + "https://raw.githubusercontent.com/NobyDa/mini/master/Color/Google.png" + ] } ], "task": [ diff --git a/QuantumultX/Snippet/GoogleCAPTCHA.snippet b/QuantumultX/Snippet/GoogleCAPTCHA.snippet new file mode 100644 index 0000000..0f7e985 --- /dev/null +++ b/QuantumultX/Snippet/GoogleCAPTCHA.snippet @@ -0,0 +1,12 @@ +# QuantumultX 远程重写配置片段 + +# Google搜索人机验证解决方案 +# Google搜索内容时并发使用多个代理策略、策略组尝试搜索内容,并返回最优结果。具体细节可查看脚本注释。 + +# 脚本:https://raw.githubusercontent.com/NobyDa/Script/master/Surge/JS/Google_CAPTCHA.js +# 片段:https://raw.githubusercontent.com/NobyDa/Script/master/QuantumultX/Snippet/GoogleCAPTCHA.snippet + + +^https:\/\/www\.google\.com(?:\.[a-z]+|)\/(?:search\?(?:|.+?&)q=|$) url script-response-body https://raw.githubusercontent.com/NobyDa/Script/master/Surge/JS/Google_CAPTCHA.js + +hostname = www.google.com* \ No newline at end of file diff --git a/Surge/JS/Google_CAPTCHA.js b/Surge/JS/Google_CAPTCHA.js index 84e8554..630e57a 100644 --- a/Surge/JS/Google_CAPTCHA.js +++ b/Surge/JS/Google_CAPTCHA.js @@ -1,65 +1,126 @@ -/* -Google搜索内容时并发使用多个策略(组),以避免可能出现的人机验证 +/******************************** +Google搜索人机验证解决方案 +搜索内容时遇到人机验证立即并发使用多个代理策略、策略组尝试搜索内容,并返回最优结果。 -注:该脚本仅兼容Surge(4.9.3+),使用时需要在脚本配置内的argument参数中填写筛选策略/组的正则表达式,留空则表示同时使用所有策略/组 +脚本作者:@NobyDa +更新时间:2024/05/16 +平台兼容:Surge(iOS4.9.3+/macOS4.2.3+) / QuantumultX(1.0.26+) -Surge脚本配置: +可在BoxJs或Surge脚本配置参数(argument)填写筛选的代理策略、策略组的正则表达式。 +所有代理策略、策略组至多筛选、使用20个,不筛选则表示随机使用。 -[Script] -Google CAPTCHA = type=http-response,pattern=^https:\/\/www\.google\.com(?:\.[a-z]+|)\/(?:search\?(?:|.+?&)q=|$),requires-body=1,debug=0,script-path=https://raw.githubusercontent.com/NobyDa/Script/master/Surge/JS/Google_CAPTCHA.js,max-size=0,timeout=10,ability=http-client-policy,argument=^(🇸🇬|🇭🇰)\s.*\d+$ +注意:Surge由于策略架构问题,正则表达式筛选的"代理策略"不包含"外部代理策略"; +QuantumultX无此限制,正则表达式可筛选所有"策略组"内的"代理策略"。 -[MITM] -hostname = www.google.com* +********************************* +Surge(iOS 5.9.0+/macOS 5.5.0+) 模块: +https://raw.githubusercontent.com/NobyDa/Script/master/Surge/Module/GoogleCAPTCHA.sgmodule -*/ +********************************* +QuantumultX(1.0.26+) 重写资源引用: +https://raw.githubusercontent.com/NobyDa/Script/master/QuantumultX/Snippet/GoogleCAPTCHA.snippet -let ret = {}; +*********************************/ -(async () => { - if ($response.status !== 200) { - const allPolicy = await new Promise((r) => { - $httpAPI("GET", "v1/policies", null, (v) => r( - [...v.proxies, ...v['policy-groups']] - )) - }); - const selected = allPolicy.filter((n) => { - return n && new RegExp(typeof $argument == 'string' ? $argument : "").test(n) - }); - console.log(`[INFO]: Use policy ${JSON.stringify(selected, null, 2)}`); - delete $request.headers.cookie; - delete $request.headers.Cookie; - const http = [ - new Promise((r, e) => setTimeout(() => e('Timeout'), 5000)), - ...selected.map( - (v) => new Promise((r, e) => { - $httpClient[$request.method.toLowerCase()]({ - url: $request.url, - headers: $request.headers, - policy: v - }, (error, response, body) => { - if (response && response.status == 200) { - r({ - policy: v, - body: { - headers: response.headers, - status: response.status, - body: body - } - }) - } else if (response && response.status == 429) { - console.log(`[ERROR]: Policy "${v}" need to CAPTCHA`); - } else if (error) { - console.log(`[ERROR]: Policy "${v}" ${error}`); - } - }) - }) - ) - ]; - await Promise.race(http).then((data) => { - ret = data.body; - console.log(`[INFO]: Use data from "${data.policy}"`); - }); - } +const $ = new NobyDa_Tools(); +$.ret = {}; + +!(async () => { + if (($response.status || $response.statusCode) == 200) return; + const req = JSON.parse(JSON.stringify($request)); + const policy = await $.policy(); + const regexText = (typeof $argument == 'string' && $argument) || + JSON.parse($.data.read('GOOGLE_CAPTCHA') || '{}').Regex || ''; // empty = all + const selected = [...policy.group, ...policy.proxy] + .filter((n) => n && new RegExp(regexText).test(n)) + .sort(() => Math.random() - 0.5).slice(0, 20); // prevent too many TCP, filtered to random select up to 20 + console.log(`[INFO]: Use policy ${JSON.stringify(selected, null, 2)}`); + await Promise.any([ + ...selected.map( + (i) => new Promise((r, e) => { + if (req.headers['User-Agent']) req.headers.Cookie = `${Math.random()}`; // prevent set-cookie + if (req.headers['user-agent']) req.headers.cookie = `${Math.random()}`; // h2 + $.http[req.method.toLowerCase()]({ + policy: i, node: i, opts: { policy: i }, // policy:surge, node:loon, opts:qx + ...req + }).then((v) => { + if (v.status == 200) { + r({ policy: i, body: { ...v, status: $.isQuanX ? 'HTTP/1.1 200' : 200 } }) + } else if (v.status == 429) { + e(console.log(`[INFO]: Policy "${i}" need to CAPTCHA`)) + } else { + e(console.log(`[INFO]: Policy "${i}" unknown resp status "${v.status}"`)) + } + }).catch((err) => e(console.log(`[ERROR]: ${err}`))) + }) + ) + ]).then((data) => { + $.ret = data.body; + console.log(`[INFO]: Use data from "${data.policy}"`); + }) })() .catch((err) => console.log(`[ERROR]: ${(err && err.message) || err}`)) - .finally(() => $done(ret)); \ No newline at end of file + .finally(() => $done($.ret)); + + +function NobyDa_Tools() { + this.isLoon = typeof $loon !== "undefined"; + this.isQuanX = typeof $configuration !== 'undefined'; + this.isSurge = typeof $environment !== 'undefined' && $environment['surge-version']; + this.isNode = typeof module !== 'undefined'&& !!module.exports; + this.http = Object.fromEntries( + ["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"].map( + (m) => [m.toLowerCase(), (opts) => { + if (this.isQuanX) return new Promise((resolve, reject) => { + $task.fetch({ method: m, ...opts }) + .then((r) => resolve({ + status: r.statusCode, headers: r.headers, body: r.body, + }), e => reject(e.error)) + }); + if (this.isSurge || this.isLoon || this.isNode) return new Promise((resolve, reject) => { + const request = this.isNode ? require("request") : $httpClient; + request[m.toLowerCase()](opts, (e, r, b) => { + if (e) reject(e); + else resolve({ status: r.status || r.statusCode, headers: r.headers, body: b }) + }) + }); + }] + ) + ); + this.policy = () => { + if (this.isSurge) return new Promise((r) => { + $httpAPI("GET", "v1/policies", null, (v) => r({ + proxy: v.proxies, + group: v['policy-groups'] + })) + }); + if (this.isQuanX) return new Promise((r) => { + $configuration.sendMessage({ + action: "get_customized_policy" + }).then(b => r({ + proxy: Object.keys(b.ret) + .reduce((t, i) => [...new Set([...t, ...b.ret[i].candidates || []])], []) + .filter((v) => !b.ret[v] && !['direct', 'proxy', 'reject'].includes(v)), + group: Object.keys(b.ret) + }), () => r({})); + }); + // if (this.isLoon) { + // const config = JSON.parse($config.getConfig()); + // const proxy = config['all_policy_groups'] + // .reduce((t, i) => [...new Set([...t, ...JSON.parse($config.getSubPolicies(i) || '[]').map(n => n.name)])], []) + // .filter((v) => ![...config['all_policy_groups'], ...config['all_buildin_nodes']].includes(v)); + // return { proxy, group: config['all_policy_groups'] } + // }; + }; + this.data = Object.fromEntries(['read', 'write'].map( + (i) => [i, (v1, v2) => { + if (i === 'write') { + if (this.isSurge || this.isLoon) return $persistentStore.write(v1, v2); + if (this.isQuanX) return $prefs.setValueForKey(v1, v2); + } else if (i === 'read') { + if (this.isSurge || this.isLoon) return $persistentStore.read(v1); + if (this.isQuanX) return $prefs.valueForKey(v1); + } + }] + )); +} \ No newline at end of file