From fd6be0871fd607176f4c74ffee4138cde2f37fcb Mon Sep 17 00:00:00 2001
From: NobyDa <53217160+NobyDa@users.noreply.github.com>
Date: Thu, 16 May 2024 20:54:44 +0800
Subject: [PATCH] Google_CAPTCHA.js now compatible with qx.
---
NobyDa_BoxJs.json | 26 ++++
QuantumultX/Snippet/GoogleCAPTCHA.snippet | 12 ++
Surge/JS/Google_CAPTCHA.js | 173 +++++++++++++++-------
3 files changed, 155 insertions(+), 56 deletions(-)
create mode 100644 QuantumultX/Snippet/GoogleCAPTCHA.snippet
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