Spaces:
Sleeping
Sleeping
Delete scripts
Browse files- scripts/check_model_catalog_sync.py +0 -108
- scripts/entrypoint.sh +0 -7
- scripts/init_storage.sh +0 -20
- scripts/smoke_test.py +0 -165
- scripts/start.sh +0 -11
- scripts/turnstile_solver/api_solver.py +0 -1047
- scripts/turnstile_solver/browser_configs.py +0 -17
- scripts/turnstile_solver/db_results.py +0 -27
- scripts/wait_for_storage.py +0 -85
scripts/check_model_catalog_sync.py
DELETED
|
@@ -1,108 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Lightweight guard for model catalog consistency across runtimes.
|
| 4 |
-
|
| 5 |
-
Checks:
|
| 6 |
-
1) app/services/grok/model.py and src/grok/models.ts expose the exact same model ids.
|
| 7 |
-
2) src/**/*.ts must not contain removed legacy model identifiers.
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
from __future__ import annotations
|
| 11 |
-
|
| 12 |
-
import re
|
| 13 |
-
import sys
|
| 14 |
-
from pathlib import Path
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
ROOT = Path(__file__).resolve().parents[1]
|
| 18 |
-
PY_MODEL_FILE = ROOT / "app" / "services" / "grok" / "model.py"
|
| 19 |
-
TS_MODEL_FILE = ROOT / "src" / "grok" / "models.ts"
|
| 20 |
-
TS_SRC_DIR = ROOT / "src"
|
| 21 |
-
|
| 22 |
-
PY_MODEL_ID_RE = re.compile(r'model_id\s*=\s*"([^"]+)"')
|
| 23 |
-
TS_MODEL_ID_RE = re.compile(r'^\s*"(?P<id>grok-[^"]+)"\s*:\s*{', re.MULTILINE)
|
| 24 |
-
|
| 25 |
-
REMOVED_IDENTIFIERS = (
|
| 26 |
-
"grok-3-fast",
|
| 27 |
-
"grok-4-fast",
|
| 28 |
-
"grok-4-mini-thinking-tahoe",
|
| 29 |
-
"grok-4.1",
|
| 30 |
-
)
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def _read_text(path: Path) -> str:
|
| 34 |
-
return path.read_text(encoding="utf-8")
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def _collect_python_model_ids(text: str) -> set[str]:
|
| 38 |
-
return {v for v in PY_MODEL_ID_RE.findall(text) if v.startswith("grok-")}
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def _collect_workers_model_ids(text: str) -> set[str]:
|
| 42 |
-
return {m.group("id") for m in TS_MODEL_ID_RE.finditer(text)}
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def _build_token_pattern(token: str) -> re.Pattern[str]:
|
| 46 |
-
# Exact token match (avoid matching prefixes like `grok-4.1-mini` for `grok-4.1`).
|
| 47 |
-
escaped = re.escape(token)
|
| 48 |
-
return re.compile(rf"(?<![A-Za-z0-9_.-]){escaped}(?![A-Za-z0-9_.-])")
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def _scan_removed_identifiers() -> list[tuple[str, int, str, str]]:
|
| 52 |
-
patterns = [(token, _build_token_pattern(token)) for token in REMOVED_IDENTIFIERS]
|
| 53 |
-
findings: list[tuple[str, int, str, str]] = []
|
| 54 |
-
|
| 55 |
-
for ts_file in sorted(TS_SRC_DIR.rglob("*.ts")):
|
| 56 |
-
text = _read_text(ts_file)
|
| 57 |
-
for line_no, line in enumerate(text.splitlines(), start=1):
|
| 58 |
-
for token, pat in patterns:
|
| 59 |
-
if pat.search(line):
|
| 60 |
-
findings.append(
|
| 61 |
-
(
|
| 62 |
-
ts_file.relative_to(ROOT).as_posix(),
|
| 63 |
-
line_no,
|
| 64 |
-
token,
|
| 65 |
-
line.strip(),
|
| 66 |
-
),
|
| 67 |
-
)
|
| 68 |
-
return findings
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def main() -> int:
|
| 72 |
-
errors: list[str] = []
|
| 73 |
-
|
| 74 |
-
py_ids = _collect_python_model_ids(_read_text(PY_MODEL_FILE))
|
| 75 |
-
ts_ids = _collect_workers_model_ids(_read_text(TS_MODEL_FILE))
|
| 76 |
-
|
| 77 |
-
if py_ids != ts_ids:
|
| 78 |
-
only_python = sorted(py_ids - ts_ids)
|
| 79 |
-
only_workers = sorted(ts_ids - py_ids)
|
| 80 |
-
lines = ["Model catalog mismatch between python and workers."]
|
| 81 |
-
if only_python:
|
| 82 |
-
lines.append("Only in app/services/grok/model.py:")
|
| 83 |
-
lines.extend(f" - {mid}" for mid in only_python)
|
| 84 |
-
if only_workers:
|
| 85 |
-
lines.append("Only in src/grok/models.ts:")
|
| 86 |
-
lines.extend(f" - {mid}" for mid in only_workers)
|
| 87 |
-
errors.append("\n".join(lines))
|
| 88 |
-
|
| 89 |
-
removed_hits = _scan_removed_identifiers()
|
| 90 |
-
if removed_hits:
|
| 91 |
-
lines = ["Removed model identifiers found under src/**/*.ts:"]
|
| 92 |
-
for rel_path, line_no, token, snippet in removed_hits:
|
| 93 |
-
lines.append(f" - {rel_path}:{line_no} -> {token}")
|
| 94 |
-
lines.append(f" {snippet}")
|
| 95 |
-
errors.append("\n".join(lines))
|
| 96 |
-
|
| 97 |
-
if errors:
|
| 98 |
-
print("[check_model_catalog_sync] FAILED", file=sys.stderr)
|
| 99 |
-
for idx, msg in enumerate(errors, start=1):
|
| 100 |
-
print(f"\n[{idx}] {msg}", file=sys.stderr)
|
| 101 |
-
return 1
|
| 102 |
-
|
| 103 |
-
print("[check_model_catalog_sync] OK: model catalogs are synchronized and legacy identifiers are absent.")
|
| 104 |
-
return 0
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
if __name__ == "__main__":
|
| 108 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/entrypoint.sh
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env sh
|
| 2 |
-
set -eu
|
| 3 |
-
|
| 4 |
-
/app/scripts/init_storage.sh
|
| 5 |
-
python /app/scripts/wait_for_storage.py
|
| 6 |
-
|
| 7 |
-
exec "$@"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/init_storage.sh
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env sh
|
| 2 |
-
set -eu
|
| 3 |
-
|
| 4 |
-
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
| 5 |
-
DATA_DIR="$ROOT_DIR/data"
|
| 6 |
-
LOG_DIR="$ROOT_DIR/logs"
|
| 7 |
-
TMP_DIR="$DATA_DIR/tmp"
|
| 8 |
-
DEFAULT_CONFIG="$ROOT_DIR/config.defaults.toml"
|
| 9 |
-
|
| 10 |
-
mkdir -p "$DATA_DIR" "$LOG_DIR" "$TMP_DIR"
|
| 11 |
-
|
| 12 |
-
if [ ! -f "$DATA_DIR/config.toml" ]; then
|
| 13 |
-
cp "$DEFAULT_CONFIG" "$DATA_DIR/config.toml"
|
| 14 |
-
fi
|
| 15 |
-
|
| 16 |
-
if [ ! -f "$DATA_DIR/token.json" ]; then
|
| 17 |
-
echo "{}" > "$DATA_DIR/token.json"
|
| 18 |
-
fi
|
| 19 |
-
|
| 20 |
-
chmod 600 "$DATA_DIR/config.toml" "$DATA_DIR/token.json" || true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/smoke_test.py
DELETED
|
@@ -1,165 +0,0 @@
|
|
| 1 |
-
import argparse
|
| 2 |
-
import json
|
| 3 |
-
import os
|
| 4 |
-
import sys
|
| 5 |
-
from typing import Any
|
| 6 |
-
from urllib.error import HTTPError, URLError
|
| 7 |
-
from urllib.request import Request, urlopen
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
def http_get(url: str, *, headers: dict[str, str] | None = None, timeout: float = 5.0) -> tuple[int, bytes]:
|
| 11 |
-
req = Request(url, headers=headers or {}, method="GET")
|
| 12 |
-
try:
|
| 13 |
-
with urlopen(req, timeout=timeout) as resp:
|
| 14 |
-
return int(getattr(resp, "status", 200)), resp.read()
|
| 15 |
-
except HTTPError as e:
|
| 16 |
-
body = b""
|
| 17 |
-
try:
|
| 18 |
-
body = e.read()
|
| 19 |
-
except Exception:
|
| 20 |
-
pass
|
| 21 |
-
return int(getattr(e, "code", 0) or 0), body
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def http_post(
|
| 25 |
-
url: str,
|
| 26 |
-
*,
|
| 27 |
-
body: bytes,
|
| 28 |
-
headers: dict[str, str] | None = None,
|
| 29 |
-
timeout: float = 5.0,
|
| 30 |
-
) -> tuple[int, dict[str, str], bytes]:
|
| 31 |
-
req = Request(url, data=body, headers=headers or {}, method="POST")
|
| 32 |
-
try:
|
| 33 |
-
with urlopen(req, timeout=timeout) as resp:
|
| 34 |
-
return (
|
| 35 |
-
int(getattr(resp, "status", 200)),
|
| 36 |
-
{k.lower(): v for k, v in dict(resp.headers).items()},
|
| 37 |
-
resp.read(),
|
| 38 |
-
)
|
| 39 |
-
except HTTPError as e:
|
| 40 |
-
body_bytes = b""
|
| 41 |
-
try:
|
| 42 |
-
body_bytes = e.read()
|
| 43 |
-
except Exception:
|
| 44 |
-
pass
|
| 45 |
-
return (
|
| 46 |
-
int(getattr(e, "code", 0) or 0),
|
| 47 |
-
{k.lower(): v for k, v in dict(e.headers).items()} if e.headers else {},
|
| 48 |
-
body_bytes,
|
| 49 |
-
)
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def require_ok(name: str, status: int, body: bytes, *, allow: set[int] | None = None) -> None:
|
| 53 |
-
allow = allow or {200}
|
| 54 |
-
if status in allow:
|
| 55 |
-
return
|
| 56 |
-
text = body.decode("utf-8", errors="replace")
|
| 57 |
-
raise SystemExit(f"[smoke] {name} failed: status={status}, body={text[:500]}")
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def try_parse_json(body: bytes) -> Any:
|
| 61 |
-
try:
|
| 62 |
-
return json.loads(body.decode("utf-8"))
|
| 63 |
-
except Exception:
|
| 64 |
-
return None
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def main() -> int:
|
| 68 |
-
parser = argparse.ArgumentParser(description="Grok2API smoke test (local/docker/cloudflare).")
|
| 69 |
-
parser.add_argument(
|
| 70 |
-
"--base-url",
|
| 71 |
-
default=os.getenv("GROK2API_BASE_URL", "http://127.0.0.1:8000"),
|
| 72 |
-
help="Base URL, e.g. http://127.0.0.1:8000",
|
| 73 |
-
)
|
| 74 |
-
parser.add_argument(
|
| 75 |
-
"--api-key",
|
| 76 |
-
default=os.getenv("GROK2API_API_KEY", ""),
|
| 77 |
-
help="Optional API key (Bearer). Enables /v1/models check.",
|
| 78 |
-
)
|
| 79 |
-
parser.add_argument(
|
| 80 |
-
"--timeout",
|
| 81 |
-
type=float,
|
| 82 |
-
default=float(os.getenv("GROK2API_SMOKE_TIMEOUT", "5")),
|
| 83 |
-
help="HTTP timeout seconds.",
|
| 84 |
-
)
|
| 85 |
-
parser.add_argument(
|
| 86 |
-
"--check-image-stream",
|
| 87 |
-
action="store_true",
|
| 88 |
-
help="Also validate /v1/images/generations stream response Content-Type.",
|
| 89 |
-
)
|
| 90 |
-
args = parser.parse_args()
|
| 91 |
-
|
| 92 |
-
base_url = (args.base_url or "").strip().rstrip("/")
|
| 93 |
-
if not base_url:
|
| 94 |
-
print("[smoke] missing --base-url", file=sys.stderr)
|
| 95 |
-
return 2
|
| 96 |
-
|
| 97 |
-
timeout = max(0.5, float(args.timeout))
|
| 98 |
-
|
| 99 |
-
# 1) /health
|
| 100 |
-
health_url = f"{base_url}/health"
|
| 101 |
-
try:
|
| 102 |
-
status, body = http_get(health_url, timeout=timeout)
|
| 103 |
-
except URLError as e:
|
| 104 |
-
raise SystemExit(f"[smoke] /health network error: {e}") from e
|
| 105 |
-
require_ok("/health", status, body)
|
| 106 |
-
parsed = try_parse_json(body) or {}
|
| 107 |
-
runtime = (parsed.get("runtime") if isinstance(parsed, dict) else None) or "unknown"
|
| 108 |
-
print(f"[smoke] /health OK (runtime={runtime})")
|
| 109 |
-
|
| 110 |
-
# 2) /login (admin UI entry)
|
| 111 |
-
login_url = f"{base_url}/login"
|
| 112 |
-
status, body = http_get(login_url, timeout=timeout)
|
| 113 |
-
require_ok("/login", status, body, allow={200, 302, 307, 308})
|
| 114 |
-
print("[smoke] /login OK")
|
| 115 |
-
|
| 116 |
-
# 3) /v1/models (requires API key)
|
| 117 |
-
api_key = (args.api_key or "").strip()
|
| 118 |
-
if api_key:
|
| 119 |
-
models_url = f"{base_url}/v1/models"
|
| 120 |
-
status, body = http_get(models_url, headers={"Authorization": f"Bearer {api_key}"}, timeout=timeout)
|
| 121 |
-
require_ok("/v1/models", status, body)
|
| 122 |
-
print("[smoke] /v1/models OK")
|
| 123 |
-
|
| 124 |
-
method_url = f"{base_url}/v1/images/method"
|
| 125 |
-
status, body = http_get(method_url, headers={"Authorization": f"Bearer {api_key}"}, timeout=timeout)
|
| 126 |
-
require_ok("/v1/images/method", status, body)
|
| 127 |
-
parsed_method = try_parse_json(body) or {}
|
| 128 |
-
method = str((parsed_method.get("image_generation_method") if isinstance(parsed_method, dict) else "") or "")
|
| 129 |
-
if method not in {"legacy", "imagine_ws_experimental"}:
|
| 130 |
-
raise SystemExit(f"[smoke] /v1/images/method unexpected value: {method!r}")
|
| 131 |
-
print(f"[smoke] /v1/images/method OK (method={method})")
|
| 132 |
-
|
| 133 |
-
if args.check_image_stream:
|
| 134 |
-
stream_url = f"{base_url}/v1/images/generations"
|
| 135 |
-
payload = {
|
| 136 |
-
"prompt": "smoke test image",
|
| 137 |
-
"model": "grok-imagine-1.0",
|
| 138 |
-
"n": 1,
|
| 139 |
-
"stream": True,
|
| 140 |
-
"response_format": "b64_json",
|
| 141 |
-
}
|
| 142 |
-
status, resp_headers, body = http_post(
|
| 143 |
-
stream_url,
|
| 144 |
-
body=json.dumps(payload).encode("utf-8"),
|
| 145 |
-
headers={
|
| 146 |
-
"Authorization": f"Bearer {api_key}",
|
| 147 |
-
"Content-Type": "application/json",
|
| 148 |
-
},
|
| 149 |
-
timeout=timeout,
|
| 150 |
-
)
|
| 151 |
-
require_ok("/v1/images/generations(stream)", status, body)
|
| 152 |
-
ctype = str(resp_headers.get("content-type", "")).lower()
|
| 153 |
-
if "text/event-stream" not in ctype:
|
| 154 |
-
raise SystemExit(
|
| 155 |
-
f"[smoke] /v1/images/generations(stream) invalid content-type: {ctype!r}"
|
| 156 |
-
)
|
| 157 |
-
print("[smoke] /v1/images/generations(stream) Content-Type OK")
|
| 158 |
-
else:
|
| 159 |
-
print("[smoke] /v1/models and /v1/images/method skipped (no --api-key)")
|
| 160 |
-
|
| 161 |
-
return 0
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
if __name__ == "__main__":
|
| 165 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/start.sh
DELETED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env sh
|
| 2 |
-
set -eu
|
| 3 |
-
|
| 4 |
-
HOST="${SERVER_HOST:-0.0.0.0}"
|
| 5 |
-
PORT="${SERVER_PORT:-8000}"
|
| 6 |
-
WORKERS="${SERVER_WORKERS:-1}"
|
| 7 |
-
|
| 8 |
-
LOG_LEVEL_LOWER="$(printf "%s" "${LOG_LEVEL:-INFO}" | tr '[:upper:]' '[:lower:]')"
|
| 9 |
-
|
| 10 |
-
exec uvicorn main:app --host "$HOST" --port "$PORT" --workers "$WORKERS" --log-level "$LOG_LEVEL_LOWER"
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/turnstile_solver/api_solver.py
DELETED
|
@@ -1,1047 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import sys
|
| 3 |
-
import time
|
| 4 |
-
import uuid
|
| 5 |
-
import random
|
| 6 |
-
import logging
|
| 7 |
-
import asyncio
|
| 8 |
-
from typing import Optional, Union
|
| 9 |
-
import argparse
|
| 10 |
-
from quart import Quart, request, jsonify
|
| 11 |
-
try:
|
| 12 |
-
from camoufox.async_api import AsyncCamoufox
|
| 13 |
-
except Exception: # pragma: no cover
|
| 14 |
-
AsyncCamoufox = None # type: ignore
|
| 15 |
-
|
| 16 |
-
try:
|
| 17 |
-
from patchright.async_api import async_playwright
|
| 18 |
-
except Exception: # pragma: no cover
|
| 19 |
-
from playwright.async_api import async_playwright
|
| 20 |
-
from db_results import init_db, save_result, load_result, cleanup_old_results
|
| 21 |
-
from browser_configs import browser_config
|
| 22 |
-
from rich.console import Console
|
| 23 |
-
from rich.panel import Panel
|
| 24 |
-
from rich.text import Text
|
| 25 |
-
from rich.align import Align
|
| 26 |
-
from rich import box
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
COLORS = {
|
| 31 |
-
'MAGENTA': '\033[35m',
|
| 32 |
-
'BLUE': '\033[34m',
|
| 33 |
-
'GREEN': '\033[32m',
|
| 34 |
-
'YELLOW': '\033[33m',
|
| 35 |
-
'RED': '\033[31m',
|
| 36 |
-
'RESET': '\033[0m',
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
class CustomLogger(logging.Logger):
|
| 41 |
-
@staticmethod
|
| 42 |
-
def format_message(level, color, message):
|
| 43 |
-
timestamp = time.strftime('%H:%M:%S')
|
| 44 |
-
return f"[{timestamp}] [{COLORS.get(color)}{level}{COLORS.get('RESET')}] -> {message}"
|
| 45 |
-
|
| 46 |
-
def debug(self, message, *args, **kwargs):
|
| 47 |
-
super().debug(self.format_message('DEBUG', 'MAGENTA', message), *args, **kwargs)
|
| 48 |
-
|
| 49 |
-
def info(self, message, *args, **kwargs):
|
| 50 |
-
super().info(self.format_message('INFO', 'BLUE', message), *args, **kwargs)
|
| 51 |
-
|
| 52 |
-
def success(self, message, *args, **kwargs):
|
| 53 |
-
super().info(self.format_message('SUCCESS', 'GREEN', message), *args, **kwargs)
|
| 54 |
-
|
| 55 |
-
def warning(self, message, *args, **kwargs):
|
| 56 |
-
super().warning(self.format_message('WARNING', 'YELLOW', message), *args, **kwargs)
|
| 57 |
-
|
| 58 |
-
def error(self, message, *args, **kwargs):
|
| 59 |
-
super().error(self.format_message('ERROR', 'RED', message), *args, **kwargs)
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
logging.setLoggerClass(CustomLogger)
|
| 63 |
-
logger: CustomLogger = logging.getLogger("TurnstileAPIServer") # type: ignore
|
| 64 |
-
logger.setLevel(logging.DEBUG)
|
| 65 |
-
handler = logging.StreamHandler(sys.stdout)
|
| 66 |
-
logger.addHandler(handler)
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
class TurnstileAPIServer:
|
| 70 |
-
|
| 71 |
-
def __init__(self, headless: bool, useragent: Optional[str], debug: bool, browser_type: str, thread: int, proxy_support: bool, use_random_config: bool = False, browser_name: Optional[str] = None, browser_version: Optional[str] = None):
|
| 72 |
-
self.app = Quart(__name__)
|
| 73 |
-
self.debug = debug
|
| 74 |
-
self.browser_type = browser_type
|
| 75 |
-
self.headless = headless
|
| 76 |
-
self.thread_count = thread
|
| 77 |
-
self.proxy_support = proxy_support
|
| 78 |
-
self.browser_pool = asyncio.Queue()
|
| 79 |
-
self.use_random_config = use_random_config
|
| 80 |
-
self.browser_name = browser_name
|
| 81 |
-
self.browser_version = browser_version
|
| 82 |
-
self.console = Console()
|
| 83 |
-
|
| 84 |
-
# Initialize useragent and sec_ch_ua attributes
|
| 85 |
-
self.useragent = useragent
|
| 86 |
-
self.sec_ch_ua = None
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
if self.browser_type in ['chromium', 'chrome', 'msedge']:
|
| 90 |
-
if browser_name and browser_version:
|
| 91 |
-
config = browser_config.get_browser_config(browser_name, browser_version)
|
| 92 |
-
if config:
|
| 93 |
-
useragent, sec_ch_ua = config
|
| 94 |
-
self.useragent = useragent
|
| 95 |
-
self.sec_ch_ua = sec_ch_ua
|
| 96 |
-
elif useragent:
|
| 97 |
-
self.useragent = useragent
|
| 98 |
-
else:
|
| 99 |
-
browser, version, useragent, sec_ch_ua = browser_config.get_random_browser_config(self.browser_type)
|
| 100 |
-
self.browser_name = browser
|
| 101 |
-
self.browser_version = version
|
| 102 |
-
self.useragent = useragent
|
| 103 |
-
self.sec_ch_ua = sec_ch_ua
|
| 104 |
-
|
| 105 |
-
self.browser_args = []
|
| 106 |
-
if self.useragent:
|
| 107 |
-
self.browser_args.append(f"--user-agent={self.useragent}")
|
| 108 |
-
|
| 109 |
-
self._setup_routes()
|
| 110 |
-
|
| 111 |
-
def display_welcome(self):
|
| 112 |
-
"""Displays welcome screen with logo."""
|
| 113 |
-
self.console.clear()
|
| 114 |
-
|
| 115 |
-
combined_text = Text()
|
| 116 |
-
combined_text.append("\nChannel: ", style="bold white")
|
| 117 |
-
combined_text.append("https://t.me/D3_vin", style="cyan")
|
| 118 |
-
combined_text.append("\nChat: ", style="bold white")
|
| 119 |
-
combined_text.append("https://t.me/D3vin_chat", style="cyan")
|
| 120 |
-
combined_text.append("\nGitHub: ", style="bold white")
|
| 121 |
-
combined_text.append("https://github.com/D3-vin", style="cyan")
|
| 122 |
-
combined_text.append("\nVersion: ", style="bold white")
|
| 123 |
-
combined_text.append("1.2a", style="green")
|
| 124 |
-
combined_text.append("\n")
|
| 125 |
-
|
| 126 |
-
info_panel = Panel(
|
| 127 |
-
Align.left(combined_text),
|
| 128 |
-
title="[bold blue]Turnstile Solver[/bold blue]",
|
| 129 |
-
subtitle="[bold magenta]Dev by D3vin[/bold magenta]",
|
| 130 |
-
box=box.ROUNDED,
|
| 131 |
-
border_style="bright_blue",
|
| 132 |
-
padding=(0, 1),
|
| 133 |
-
width=50
|
| 134 |
-
)
|
| 135 |
-
|
| 136 |
-
try:
|
| 137 |
-
self.console.print(info_panel)
|
| 138 |
-
self.console.print()
|
| 139 |
-
except UnicodeEncodeError:
|
| 140 |
-
# Fallback for Windows consoles with non-UTF8 encoding
|
| 141 |
-
print("Turnstile Solver")
|
| 142 |
-
print("Channel: https://t.me/D3_vin")
|
| 143 |
-
print("Chat: https://t.me/D3vin_chat")
|
| 144 |
-
print("GitHub: https://github.com/D3-vin")
|
| 145 |
-
print("Version: 1.2a")
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
def _setup_routes(self) -> None:
|
| 151 |
-
"""Set up the application routes."""
|
| 152 |
-
self.app.before_serving(self._startup)
|
| 153 |
-
self.app.route('/turnstile', methods=['GET'])(self.process_turnstile)
|
| 154 |
-
self.app.route('/result', methods=['GET'])(self.get_result)
|
| 155 |
-
self.app.route('/')(self.index)
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
async def _startup(self) -> None:
|
| 159 |
-
"""Initialize the browser and page pool on startup."""
|
| 160 |
-
self.display_welcome()
|
| 161 |
-
logger.info("Starting browser initialization")
|
| 162 |
-
try:
|
| 163 |
-
await init_db()
|
| 164 |
-
await self._initialize_browser()
|
| 165 |
-
|
| 166 |
-
# Запускаем периодическую очистку старых результатов
|
| 167 |
-
asyncio.create_task(self._periodic_cleanup())
|
| 168 |
-
|
| 169 |
-
except Exception as e:
|
| 170 |
-
logger.error(f"Failed to initialize browser: {str(e)}")
|
| 171 |
-
raise
|
| 172 |
-
|
| 173 |
-
async def _initialize_browser(self) -> None:
|
| 174 |
-
"""Initialize the browser and create the page pool."""
|
| 175 |
-
playwright = None
|
| 176 |
-
camoufox = None
|
| 177 |
-
|
| 178 |
-
if self.browser_type in ['chromium', 'chrome', 'msedge']:
|
| 179 |
-
playwright = await async_playwright().start()
|
| 180 |
-
elif self.browser_type == "camoufox":
|
| 181 |
-
if AsyncCamoufox is None:
|
| 182 |
-
raise RuntimeError("camoufox is not installed. Please install camoufox or use --browser_type chromium.")
|
| 183 |
-
camoufox = AsyncCamoufox(headless=self.headless)
|
| 184 |
-
|
| 185 |
-
browser_configs = []
|
| 186 |
-
for _ in range(self.thread_count):
|
| 187 |
-
if self.browser_type in ['chromium', 'chrome', 'msedge']:
|
| 188 |
-
if self.use_random_config:
|
| 189 |
-
browser, version, useragent, sec_ch_ua = browser_config.get_random_browser_config(self.browser_type)
|
| 190 |
-
elif self.browser_name and self.browser_version:
|
| 191 |
-
config = browser_config.get_browser_config(self.browser_name, self.browser_version)
|
| 192 |
-
if config:
|
| 193 |
-
useragent, sec_ch_ua = config
|
| 194 |
-
browser = self.browser_name
|
| 195 |
-
version = self.browser_version
|
| 196 |
-
else:
|
| 197 |
-
browser, version, useragent, sec_ch_ua = browser_config.get_random_browser_config(self.browser_type)
|
| 198 |
-
else:
|
| 199 |
-
browser = getattr(self, 'browser_name', 'custom')
|
| 200 |
-
version = getattr(self, 'browser_version', 'custom')
|
| 201 |
-
useragent = self.useragent
|
| 202 |
-
sec_ch_ua = getattr(self, 'sec_ch_ua', '')
|
| 203 |
-
else:
|
| 204 |
-
# Для camoufox и других браузеров используем значения по умолчанию
|
| 205 |
-
browser = self.browser_type
|
| 206 |
-
version = 'custom'
|
| 207 |
-
useragent = self.useragent
|
| 208 |
-
sec_ch_ua = getattr(self, 'sec_ch_ua', '')
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
browser_configs.append({
|
| 212 |
-
'browser_name': browser,
|
| 213 |
-
'browser_version': version,
|
| 214 |
-
'useragent': useragent,
|
| 215 |
-
'sec_ch_ua': sec_ch_ua
|
| 216 |
-
})
|
| 217 |
-
|
| 218 |
-
for i in range(self.thread_count):
|
| 219 |
-
config = browser_configs[i]
|
| 220 |
-
|
| 221 |
-
browser_args = [
|
| 222 |
-
"--window-position=0,0",
|
| 223 |
-
"--force-device-scale-factor=1"
|
| 224 |
-
]
|
| 225 |
-
if config['useragent']:
|
| 226 |
-
browser_args.append(f"--user-agent={config['useragent']}")
|
| 227 |
-
|
| 228 |
-
browser = None
|
| 229 |
-
if self.browser_type in ['chromium', 'chrome', 'msedge'] and playwright:
|
| 230 |
-
launch_kwargs = {
|
| 231 |
-
"headless": self.headless,
|
| 232 |
-
"args": browser_args,
|
| 233 |
-
}
|
| 234 |
-
# Only pass `channel` for branded browsers. Playwright's bundled Chromium does not use channel.
|
| 235 |
-
if self.browser_type in ["chrome", "msedge"]:
|
| 236 |
-
launch_kwargs["channel"] = self.browser_type
|
| 237 |
-
browser = await playwright.chromium.launch(**launch_kwargs)
|
| 238 |
-
elif self.browser_type == "camoufox" and camoufox:
|
| 239 |
-
browser = await camoufox.start()
|
| 240 |
-
|
| 241 |
-
if browser:
|
| 242 |
-
await self.browser_pool.put((i+1, browser, config))
|
| 243 |
-
|
| 244 |
-
if self.debug:
|
| 245 |
-
logger.info(f"Browser {i + 1} initialized successfully with {config['browser_name']} {config['browser_version']}")
|
| 246 |
-
|
| 247 |
-
logger.info(f"Browser pool initialized with {self.browser_pool.qsize()} browsers")
|
| 248 |
-
|
| 249 |
-
if self.use_random_config:
|
| 250 |
-
logger.info(f"Each browser in pool received random configuration")
|
| 251 |
-
elif self.browser_name and self.browser_version:
|
| 252 |
-
logger.info(f"All browsers using configuration: {self.browser_name} {self.browser_version}")
|
| 253 |
-
else:
|
| 254 |
-
logger.info("Using custom configuration")
|
| 255 |
-
|
| 256 |
-
if self.debug:
|
| 257 |
-
for i, config in enumerate(browser_configs):
|
| 258 |
-
logger.debug(f"Browser {i+1} config: {config['browser_name']} {config['browser_version']}")
|
| 259 |
-
logger.debug(f"Browser {i+1} User-Agent: {config['useragent']}")
|
| 260 |
-
logger.debug(f"Browser {i+1} Sec-CH-UA: {config['sec_ch_ua']}")
|
| 261 |
-
|
| 262 |
-
async def _periodic_cleanup(self):
|
| 263 |
-
"""Periodic cleanup of old results every hour"""
|
| 264 |
-
while True:
|
| 265 |
-
try:
|
| 266 |
-
await asyncio.sleep(3600)
|
| 267 |
-
deleted_count = await cleanup_old_results(days_old=7)
|
| 268 |
-
if deleted_count > 0:
|
| 269 |
-
logger.info(f"Cleaned up {deleted_count} old results")
|
| 270 |
-
except Exception as e:
|
| 271 |
-
logger.error(f"Error during periodic cleanup: {e}")
|
| 272 |
-
|
| 273 |
-
async def _antishadow_inject(self, page):
|
| 274 |
-
await page.add_init_script("""
|
| 275 |
-
(function() {
|
| 276 |
-
const originalAttachShadow = Element.prototype.attachShadow;
|
| 277 |
-
Element.prototype.attachShadow = function(init) {
|
| 278 |
-
const shadow = originalAttachShadow.call(this, init);
|
| 279 |
-
if (init.mode === 'closed') {
|
| 280 |
-
window.__lastClosedShadowRoot = shadow;
|
| 281 |
-
}
|
| 282 |
-
return shadow;
|
| 283 |
-
};
|
| 284 |
-
})();
|
| 285 |
-
""")
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
async def _optimized_route_handler(self, route):
|
| 290 |
-
"""Оптимизированный обработчик маршрутов для экономии ресурсов."""
|
| 291 |
-
url = route.request.url
|
| 292 |
-
resource_type = route.request.resource_type
|
| 293 |
-
|
| 294 |
-
allowed_types = {'document', 'script', 'xhr', 'fetch'}
|
| 295 |
-
|
| 296 |
-
allowed_domains = [
|
| 297 |
-
'challenges.cloudflare.com',
|
| 298 |
-
'static.cloudflareinsights.com',
|
| 299 |
-
'cloudflare.com'
|
| 300 |
-
]
|
| 301 |
-
|
| 302 |
-
if resource_type in allowed_types:
|
| 303 |
-
await route.continue_()
|
| 304 |
-
elif any(domain in url for domain in allowed_domains):
|
| 305 |
-
await route.continue_()
|
| 306 |
-
else:
|
| 307 |
-
await route.abort()
|
| 308 |
-
|
| 309 |
-
async def _block_rendering(self, page):
|
| 310 |
-
"""Блокировка рендеринга для экономии ресурсов"""
|
| 311 |
-
await page.route("**/*", self._optimized_route_handler)
|
| 312 |
-
|
| 313 |
-
async def _unblock_rendering(self, page):
|
| 314 |
-
"""Разблокировка рендеринга"""
|
| 315 |
-
await page.unroute("**/*", self._optimized_route_handler)
|
| 316 |
-
|
| 317 |
-
async def _find_turnstile_elements(self, page, index: int):
|
| 318 |
-
"""Умная проверка всех возможных Turnstile элементов"""
|
| 319 |
-
selectors = [
|
| 320 |
-
'.cf-turnstile',
|
| 321 |
-
'[data-sitekey]',
|
| 322 |
-
'iframe[src*="turnstile"]',
|
| 323 |
-
'iframe[title*="widget"]',
|
| 324 |
-
'div[id*="turnstile"]',
|
| 325 |
-
'div[class*="turnstile"]'
|
| 326 |
-
]
|
| 327 |
-
|
| 328 |
-
elements = []
|
| 329 |
-
for selector in selectors:
|
| 330 |
-
try:
|
| 331 |
-
# Безопасная проверка count()
|
| 332 |
-
try:
|
| 333 |
-
count = await page.locator(selector).count()
|
| 334 |
-
except Exception:
|
| 335 |
-
# Если count() дает ошибку, пропускаем этот селектор
|
| 336 |
-
continue
|
| 337 |
-
|
| 338 |
-
if count > 0:
|
| 339 |
-
elements.append((selector, count))
|
| 340 |
-
if self.debug:
|
| 341 |
-
logger.debug(f"Browser {index}: Found {count} elements with selector '{selector}'")
|
| 342 |
-
except Exception as e:
|
| 343 |
-
if self.debug:
|
| 344 |
-
logger.debug(f"Browser {index}: Selector '{selector}' failed: {str(e)}")
|
| 345 |
-
continue
|
| 346 |
-
|
| 347 |
-
return elements
|
| 348 |
-
|
| 349 |
-
async def _find_and_click_checkbox(self, page, index: int):
|
| 350 |
-
"""Найти и кликнуть по чекбоксу Turnstile CAPTCHA внутри iframe"""
|
| 351 |
-
try:
|
| 352 |
-
# Пробуем разные селекторы iframe с защитой от ошибок
|
| 353 |
-
iframe_selectors = [
|
| 354 |
-
'iframe[src*="challenges.cloudflare.com"]',
|
| 355 |
-
'iframe[src*="turnstile"]',
|
| 356 |
-
'iframe[title*="widget"]'
|
| 357 |
-
]
|
| 358 |
-
|
| 359 |
-
iframe_locator = None
|
| 360 |
-
for selector in iframe_selectors:
|
| 361 |
-
try:
|
| 362 |
-
test_locator = page.locator(selector).first
|
| 363 |
-
# Безопасная проверка count для iframe
|
| 364 |
-
try:
|
| 365 |
-
iframe_count = await test_locator.count()
|
| 366 |
-
except Exception:
|
| 367 |
-
iframe_count = 0
|
| 368 |
-
|
| 369 |
-
if iframe_count > 0:
|
| 370 |
-
iframe_locator = test_locator
|
| 371 |
-
if self.debug:
|
| 372 |
-
logger.debug(f"Browser {index}: Found Turnstile iframe with selector: {selector}")
|
| 373 |
-
break
|
| 374 |
-
except Exception as e:
|
| 375 |
-
if self.debug:
|
| 376 |
-
logger.debug(f"Browser {index}: Iframe selector '{selector}' failed: {str(e)}")
|
| 377 |
-
continue
|
| 378 |
-
|
| 379 |
-
if iframe_locator:
|
| 380 |
-
try:
|
| 381 |
-
# Получаем frame из iframe
|
| 382 |
-
iframe_element = await iframe_locator.element_handle()
|
| 383 |
-
frame = await iframe_element.content_frame()
|
| 384 |
-
|
| 385 |
-
if frame:
|
| 386 |
-
# Ищем чекбокс внутри iframe
|
| 387 |
-
checkbox_selectors = [
|
| 388 |
-
'input[type="checkbox"]',
|
| 389 |
-
'.cb-lb input[type="checkbox"]',
|
| 390 |
-
'label input[type="checkbox"]'
|
| 391 |
-
]
|
| 392 |
-
|
| 393 |
-
for selector in checkbox_selectors:
|
| 394 |
-
try:
|
| 395 |
-
# Полностью избегаем locator.count() в iframe - используем альтернативный подход
|
| 396 |
-
try:
|
| 397 |
-
# Пробуем кликнуть напрямую без count проверки
|
| 398 |
-
checkbox = frame.locator(selector).first
|
| 399 |
-
await checkbox.click(timeout=2000)
|
| 400 |
-
if self.debug:
|
| 401 |
-
logger.debug(f"Browser {index}: Successfully clicked checkbox in iframe with selector '{selector}'")
|
| 402 |
-
return True
|
| 403 |
-
except Exception as click_e:
|
| 404 |
-
# Если прямой клик не сработал, записываем в debug но не падаем
|
| 405 |
-
if self.debug:
|
| 406 |
-
logger.debug(f"Browser {index}: Direct checkbox click failed for '{selector}': {str(click_e)}")
|
| 407 |
-
continue
|
| 408 |
-
except Exception as e:
|
| 409 |
-
if self.debug:
|
| 410 |
-
logger.debug(f"Browser {index}: Iframe checkbox selector '{selector}' failed: {str(e)}")
|
| 411 |
-
continue
|
| 412 |
-
|
| 413 |
-
# Если нашли iframe, но не смогли кликнуть чекбокс, пробуем клик по iframe
|
| 414 |
-
try:
|
| 415 |
-
if self.debug:
|
| 416 |
-
logger.debug(f"Browser {index}: Trying to click iframe directly as fallback")
|
| 417 |
-
await iframe_locator.click(timeout=1000)
|
| 418 |
-
return True
|
| 419 |
-
except Exception as e:
|
| 420 |
-
if self.debug:
|
| 421 |
-
logger.debug(f"Browser {index}: Iframe direct click failed: {str(e)}")
|
| 422 |
-
|
| 423 |
-
except Exception as e:
|
| 424 |
-
if self.debug:
|
| 425 |
-
logger.debug(f"Browser {index}: Failed to access iframe content: {str(e)}")
|
| 426 |
-
|
| 427 |
-
except Exception as e:
|
| 428 |
-
if self.debug:
|
| 429 |
-
logger.debug(f"Browser {index}: General iframe search failed: {str(e)}")
|
| 430 |
-
|
| 431 |
-
return False
|
| 432 |
-
|
| 433 |
-
async def _try_click_strategies(self, page, index: int):
|
| 434 |
-
strategies = [
|
| 435 |
-
('checkbox_click', lambda: self._find_and_click_checkbox(page, index)),
|
| 436 |
-
('direct_widget', lambda: self._safe_click(page, '.cf-turnstile', index)),
|
| 437 |
-
('iframe_click', lambda: self._safe_click(page, 'iframe[src*="turnstile"]', index)),
|
| 438 |
-
('js_click', lambda: page.evaluate("document.querySelector('.cf-turnstile')?.click()")),
|
| 439 |
-
('sitekey_attr', lambda: self._safe_click(page, '[data-sitekey]', index)),
|
| 440 |
-
('any_turnstile', lambda: self._safe_click(page, '*[class*="turnstile"]', index)),
|
| 441 |
-
('xpath_click', lambda: self._safe_click(page, "//div[@class='cf-turnstile']", index))
|
| 442 |
-
]
|
| 443 |
-
|
| 444 |
-
for strategy_name, strategy_func in strategies:
|
| 445 |
-
try:
|
| 446 |
-
result = await strategy_func()
|
| 447 |
-
if result is True or result is None: # None означает успех для большинства стратегий
|
| 448 |
-
if self.debug:
|
| 449 |
-
logger.debug(f"Browser {index}: Click strategy '{strategy_name}' succeeded")
|
| 450 |
-
return True
|
| 451 |
-
except Exception as e:
|
| 452 |
-
if self.debug:
|
| 453 |
-
logger.debug(f"Browser {index}: Click strategy '{strategy_name}' failed: {str(e)}")
|
| 454 |
-
continue
|
| 455 |
-
|
| 456 |
-
return False
|
| 457 |
-
|
| 458 |
-
async def _safe_click(self, page, selector: str, index: int):
|
| 459 |
-
"""Полностью безопасный клик с максимальной защитой от ошибо��"""
|
| 460 |
-
try:
|
| 461 |
-
# Пробуем кликнуть напрямую без count() проверки
|
| 462 |
-
locator = page.locator(selector).first
|
| 463 |
-
await locator.click(timeout=1000)
|
| 464 |
-
return True
|
| 465 |
-
except Exception as e:
|
| 466 |
-
# Логируем ошибку только в debug режиме
|
| 467 |
-
if self.debug and "Can't query n-th element" not in str(e):
|
| 468 |
-
logger.debug(f"Browser {index}: Safe click failed for '{selector}': {str(e)}")
|
| 469 |
-
return False
|
| 470 |
-
|
| 471 |
-
async def _inject_captcha_directly(self, page, websiteKey: str, action: str = '', cdata: str = '', index: int = 0):
|
| 472 |
-
"""Inject CAPTCHA directly into the target website"""
|
| 473 |
-
script = f"""
|
| 474 |
-
// Remove any existing turnstile widgets first
|
| 475 |
-
document.querySelectorAll('.cf-turnstile').forEach(el => el.remove());
|
| 476 |
-
document.querySelectorAll('[data-sitekey]').forEach(el => el.remove());
|
| 477 |
-
|
| 478 |
-
// Create turnstile widget directly on the page
|
| 479 |
-
const captchaDiv = document.createElement('div');
|
| 480 |
-
captchaDiv.className = 'cf-turnstile';
|
| 481 |
-
captchaDiv.setAttribute('data-sitekey', '{websiteKey}');
|
| 482 |
-
captchaDiv.setAttribute('data-callback', 'onTurnstileCallback');
|
| 483 |
-
{f'captchaDiv.setAttribute("data-action", "{action}");' if action else ''}
|
| 484 |
-
{f'captchaDiv.setAttribute("data-cdata", "{cdata}");' if cdata else ''}
|
| 485 |
-
captchaDiv.style.position = 'fixed';
|
| 486 |
-
captchaDiv.style.top = '20px';
|
| 487 |
-
captchaDiv.style.left = '20px';
|
| 488 |
-
captchaDiv.style.zIndex = '9999';
|
| 489 |
-
captchaDiv.style.backgroundColor = 'white';
|
| 490 |
-
captchaDiv.style.padding = '15px';
|
| 491 |
-
captchaDiv.style.border = '2px solid #0f79af';
|
| 492 |
-
captchaDiv.style.borderRadius = '8px';
|
| 493 |
-
captchaDiv.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
|
| 494 |
-
|
| 495 |
-
// Add to body immediately
|
| 496 |
-
document.body.appendChild(captchaDiv);
|
| 497 |
-
|
| 498 |
-
// Load Turnstile script and render widget
|
| 499 |
-
const loadTurnstile = () => {{
|
| 500 |
-
const script = document.createElement('script');
|
| 501 |
-
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
|
| 502 |
-
script.async = true;
|
| 503 |
-
script.defer = true;
|
| 504 |
-
script.onload = function() {{
|
| 505 |
-
console.log('Turnstile script loaded');
|
| 506 |
-
// Wait a bit for script to initialize
|
| 507 |
-
setTimeout(() => {{
|
| 508 |
-
if (window.turnstile && window.turnstile.render) {{
|
| 509 |
-
try {{
|
| 510 |
-
window.turnstile.render(captchaDiv, {{
|
| 511 |
-
sitekey: '{websiteKey}',
|
| 512 |
-
{f'action: "{action}",' if action else ''}
|
| 513 |
-
{f'cdata: "{cdata}",' if cdata else ''}
|
| 514 |
-
callback: function(token) {{
|
| 515 |
-
console.log('Turnstile solved with token:', token);
|
| 516 |
-
// Create hidden input for token
|
| 517 |
-
let tokenInput = document.querySelector('input[name="cf-turnstile-response"]');
|
| 518 |
-
if (!tokenInput) {{
|
| 519 |
-
tokenInput = document.createElement('input');
|
| 520 |
-
tokenInput.type = 'hidden';
|
| 521 |
-
tokenInput.name = 'cf-turnstile-response';
|
| 522 |
-
document.body.appendChild(tokenInput);
|
| 523 |
-
}}
|
| 524 |
-
tokenInput.value = token;
|
| 525 |
-
}},
|
| 526 |
-
'error-callback': function(error) {{
|
| 527 |
-
console.log('Turnstile error:', error);
|
| 528 |
-
}}
|
| 529 |
-
}});
|
| 530 |
-
}} catch (e) {{
|
| 531 |
-
console.log('Turnstile render error:', e);
|
| 532 |
-
}}
|
| 533 |
-
}} else {{
|
| 534 |
-
console.log('Turnstile API not available');
|
| 535 |
-
}}
|
| 536 |
-
}}, 1000);
|
| 537 |
-
}};
|
| 538 |
-
script.onerror = function() {{
|
| 539 |
-
console.log('Failed to load Turnstile script');
|
| 540 |
-
}};
|
| 541 |
-
document.head.appendChild(script);
|
| 542 |
-
}};
|
| 543 |
-
|
| 544 |
-
// Check if Turnstile is already loaded
|
| 545 |
-
if (window.turnstile) {{
|
| 546 |
-
console.log('Turnstile already loaded, rendering immediately');
|
| 547 |
-
try {{
|
| 548 |
-
window.turnstile.render(captchaDiv, {{
|
| 549 |
-
sitekey: '{websiteKey}',
|
| 550 |
-
{f'action: "{action}",' if action else ''}
|
| 551 |
-
{f'cdata: "{cdata}",' if cdata else ''}
|
| 552 |
-
callback: function(token) {{
|
| 553 |
-
console.log('Turnstile solved with token:', token);
|
| 554 |
-
let tokenInput = document.querySelector('input[name="cf-turnstile-response"]');
|
| 555 |
-
if (!tokenInput) {{
|
| 556 |
-
tokenInput = document.createElement('input');
|
| 557 |
-
tokenInput.type = 'hidden';
|
| 558 |
-
tokenInput.name = 'cf-turnstile-response';
|
| 559 |
-
document.body.appendChild(tokenInput);
|
| 560 |
-
}}
|
| 561 |
-
tokenInput.value = token;
|
| 562 |
-
}},
|
| 563 |
-
'error-callback': function(error) {{
|
| 564 |
-
console.log('Turnstile error:', error);
|
| 565 |
-
}}
|
| 566 |
-
}});
|
| 567 |
-
}} catch (e) {{
|
| 568 |
-
console.log('Immediate render error:', e);
|
| 569 |
-
loadTurnstile();
|
| 570 |
-
}}
|
| 571 |
-
}} else {{
|
| 572 |
-
loadTurnstile();
|
| 573 |
-
}}
|
| 574 |
-
|
| 575 |
-
// Setup global callback
|
| 576 |
-
window.onTurnstileCallback = function(token) {{
|
| 577 |
-
console.log('Global turnstile callback executed:', token);
|
| 578 |
-
}};
|
| 579 |
-
"""
|
| 580 |
-
|
| 581 |
-
await page.evaluate(script)
|
| 582 |
-
if self.debug:
|
| 583 |
-
logger.debug(f"Browser {index}: Injected CAPTCHA directly into website with sitekey: {websiteKey}")
|
| 584 |
-
|
| 585 |
-
async def _solve_turnstile(self, task_id: str, url: str, sitekey: str, action: Optional[str] = None, cdata: Optional[str] = None):
|
| 586 |
-
"""Solve the Turnstile challenge."""
|
| 587 |
-
proxy = None
|
| 588 |
-
|
| 589 |
-
index, browser, browser_config = await self.browser_pool.get()
|
| 590 |
-
|
| 591 |
-
try:
|
| 592 |
-
if hasattr(browser, 'is_connected') and not browser.is_connected():
|
| 593 |
-
if self.debug:
|
| 594 |
-
logger.warning(f"Browser {index}: Browser disconnected, skipping")
|
| 595 |
-
await self.browser_pool.put((index, browser, browser_config))
|
| 596 |
-
await save_result(task_id, "turnstile", {"value": "CAPTCHA_FAIL", "elapsed_time": 0})
|
| 597 |
-
return
|
| 598 |
-
except Exception as e:
|
| 599 |
-
if self.debug:
|
| 600 |
-
logger.warning(f"Browser {index}: Cannot check browser state: {str(e)}")
|
| 601 |
-
|
| 602 |
-
if self.proxy_support:
|
| 603 |
-
proxy_file_path = os.path.join(os.getcwd(), "proxies.txt")
|
| 604 |
-
|
| 605 |
-
try:
|
| 606 |
-
with open(proxy_file_path) as proxy_file:
|
| 607 |
-
proxies = [line.strip() for line in proxy_file if line.strip()]
|
| 608 |
-
|
| 609 |
-
proxy = random.choice(proxies) if proxies else None
|
| 610 |
-
|
| 611 |
-
if self.debug and proxy:
|
| 612 |
-
logger.debug(f"Browser {index}: Selected proxy: {proxy}")
|
| 613 |
-
elif self.debug and not proxy:
|
| 614 |
-
logger.debug(f"Browser {index}: No proxies available")
|
| 615 |
-
|
| 616 |
-
except FileNotFoundError:
|
| 617 |
-
logger.warning(f"Proxy file not found: {proxy_file_path}")
|
| 618 |
-
proxy = None
|
| 619 |
-
except Exception as e:
|
| 620 |
-
logger.error(f"Error reading proxy file: {str(e)}")
|
| 621 |
-
proxy = None
|
| 622 |
-
|
| 623 |
-
if proxy:
|
| 624 |
-
if '@' in proxy:
|
| 625 |
-
try:
|
| 626 |
-
scheme_part, auth_part = proxy.split('://')
|
| 627 |
-
auth, address = auth_part.split('@')
|
| 628 |
-
username, password = auth.split(':')
|
| 629 |
-
ip, port = address.split(':')
|
| 630 |
-
if self.debug:
|
| 631 |
-
logger.debug(f"Browser {index}: Creating context with proxy {scheme_part}://{ip}:{port} (auth: {username}:***)")
|
| 632 |
-
context_options = {
|
| 633 |
-
"proxy": {
|
| 634 |
-
"server": f"{scheme_part}://{ip}:{port}",
|
| 635 |
-
"username": username,
|
| 636 |
-
"password": password
|
| 637 |
-
},
|
| 638 |
-
"user_agent": browser_config['useragent']
|
| 639 |
-
}
|
| 640 |
-
|
| 641 |
-
if browser_config['sec_ch_ua'] and browser_config['sec_ch_ua'].strip():
|
| 642 |
-
context_options['extra_http_headers'] = {
|
| 643 |
-
'sec-ch-ua': browser_config['sec_ch_ua']
|
| 644 |
-
}
|
| 645 |
-
|
| 646 |
-
context = await browser.new_context(**context_options)
|
| 647 |
-
except ValueError:
|
| 648 |
-
raise ValueError(f"Invalid proxy format: {proxy}")
|
| 649 |
-
else:
|
| 650 |
-
parts = proxy.split(':')
|
| 651 |
-
if len(parts) == 5:
|
| 652 |
-
proxy_scheme, proxy_ip, proxy_port, proxy_user, proxy_pass = parts
|
| 653 |
-
if self.debug:
|
| 654 |
-
logger.debug(f"Browser {index}: Creating context with proxy {proxy_scheme}://{proxy_ip}:{proxy_port} (auth: {proxy_user}:***)")
|
| 655 |
-
context_options = {
|
| 656 |
-
"proxy": {
|
| 657 |
-
"server": f"{proxy_scheme}://{proxy_ip}:{proxy_port}",
|
| 658 |
-
"username": proxy_user,
|
| 659 |
-
"password": proxy_pass
|
| 660 |
-
},
|
| 661 |
-
"user_agent": browser_config['useragent']
|
| 662 |
-
}
|
| 663 |
-
|
| 664 |
-
if browser_config['sec_ch_ua'] and browser_config['sec_ch_ua'].strip():
|
| 665 |
-
context_options['extra_http_headers'] = {
|
| 666 |
-
'sec-ch-ua': browser_config['sec_ch_ua']
|
| 667 |
-
}
|
| 668 |
-
|
| 669 |
-
context = await browser.new_context(**context_options)
|
| 670 |
-
elif len(parts) == 3:
|
| 671 |
-
if self.debug:
|
| 672 |
-
logger.debug(f"Browser {index}: Creating context with proxy {proxy}")
|
| 673 |
-
context_options = {
|
| 674 |
-
"proxy": {"server": f"{proxy}"},
|
| 675 |
-
"user_agent": browser_config['useragent']
|
| 676 |
-
}
|
| 677 |
-
|
| 678 |
-
if browser_config['sec_ch_ua'] and browser_config['sec_ch_ua'].strip():
|
| 679 |
-
context_options['extra_http_headers'] = {
|
| 680 |
-
'sec-ch-ua': browser_config['sec_ch_ua']
|
| 681 |
-
}
|
| 682 |
-
|
| 683 |
-
context = await browser.new_context(**context_options)
|
| 684 |
-
else:
|
| 685 |
-
raise ValueError(f"Invalid proxy format: {proxy}")
|
| 686 |
-
else:
|
| 687 |
-
if self.debug:
|
| 688 |
-
logger.debug(f"Browser {index}: Creating context without proxy")
|
| 689 |
-
context_options = {"user_agent": browser_config['useragent']}
|
| 690 |
-
|
| 691 |
-
if browser_config['sec_ch_ua'] and browser_config['sec_ch_ua'].strip():
|
| 692 |
-
context_options['extra_http_headers'] = {
|
| 693 |
-
'sec-ch-ua': browser_config['sec_ch_ua']
|
| 694 |
-
}
|
| 695 |
-
|
| 696 |
-
context = await browser.new_context(**context_options)
|
| 697 |
-
else:
|
| 698 |
-
context_options = {"user_agent": browser_config['useragent']}
|
| 699 |
-
|
| 700 |
-
if browser_config['sec_ch_ua'] and browser_config['sec_ch_ua'].strip():
|
| 701 |
-
context_options['extra_http_headers'] = {
|
| 702 |
-
'sec-ch-ua': browser_config['sec_ch_ua']
|
| 703 |
-
}
|
| 704 |
-
|
| 705 |
-
context = await browser.new_context(**context_options)
|
| 706 |
-
|
| 707 |
-
page = await context.new_page()
|
| 708 |
-
|
| 709 |
-
await self._antishadow_inject(page)
|
| 710 |
-
|
| 711 |
-
await self._block_rendering(page)
|
| 712 |
-
|
| 713 |
-
await page.add_init_script("""
|
| 714 |
-
Object.defineProperty(navigator, 'webdriver', {
|
| 715 |
-
get: () => undefined,
|
| 716 |
-
});
|
| 717 |
-
|
| 718 |
-
window.chrome = {
|
| 719 |
-
runtime: {},
|
| 720 |
-
loadTimes: function() {},
|
| 721 |
-
csi: function() {},
|
| 722 |
-
};
|
| 723 |
-
""")
|
| 724 |
-
|
| 725 |
-
if self.browser_type in ['chromium', 'chrome', 'msedge']:
|
| 726 |
-
await page.set_viewport_size({"width": 500, "height": 100})
|
| 727 |
-
if self.debug:
|
| 728 |
-
logger.debug(f"Browser {index}: Set viewport size to 500x240")
|
| 729 |
-
|
| 730 |
-
start_time = time.time()
|
| 731 |
-
|
| 732 |
-
try:
|
| 733 |
-
if self.debug:
|
| 734 |
-
logger.debug(f"Browser {index}: Starting Turnstile solve for URL: {url} with Sitekey: {sitekey} | Action: {action} | Cdata: {cdata} | Proxy: {proxy}")
|
| 735 |
-
logger.debug(f"Browser {index}: Setting up optimized page loading with resource blocking")
|
| 736 |
-
|
| 737 |
-
if self.debug:
|
| 738 |
-
logger.debug(f"Browser {index}: Loading real website directly: {url}")
|
| 739 |
-
|
| 740 |
-
await page.goto(url, wait_until='domcontentloaded', timeout=30000)
|
| 741 |
-
|
| 742 |
-
await self._unblock_rendering(page)
|
| 743 |
-
|
| 744 |
-
# Сразу инъектируем виджет Turnstile на целевой сайт
|
| 745 |
-
if self.debug:
|
| 746 |
-
logger.debug(f"Browser {index}: Injecting Turnstile widget directly into target site")
|
| 747 |
-
|
| 748 |
-
await self._inject_captcha_directly(page, sitekey, action or '', cdata or '', index)
|
| 749 |
-
|
| 750 |
-
# Ждем время для загрузки и рендеринга виджета
|
| 751 |
-
await asyncio.sleep(3)
|
| 752 |
-
|
| 753 |
-
locator = page.locator('input[name="cf-turnstile-response"]')
|
| 754 |
-
max_attempts = 30
|
| 755 |
-
click_count = 0
|
| 756 |
-
max_clicks = 10
|
| 757 |
-
|
| 758 |
-
for attempt in range(max_attempts):
|
| 759 |
-
try:
|
| 760 |
-
# Безопасная проверка количества элементов с токеном
|
| 761 |
-
try:
|
| 762 |
-
count = await locator.count()
|
| 763 |
-
except Exception as e:
|
| 764 |
-
if self.debug:
|
| 765 |
-
logger.debug(f"Browser {index}: Locator count failed on attempt {attempt + 1}: {str(e)}")
|
| 766 |
-
count = 0
|
| 767 |
-
|
| 768 |
-
if count == 0:
|
| 769 |
-
if self.debug and attempt % 5 == 0:
|
| 770 |
-
logger.debug(f"Browser {index}: No token elements found on attempt {attempt + 1}")
|
| 771 |
-
elif count == 1:
|
| 772 |
-
# Если только од��н элемент, проверяем его токен
|
| 773 |
-
try:
|
| 774 |
-
token = await locator.input_value(timeout=500)
|
| 775 |
-
if token:
|
| 776 |
-
elapsed_time = round(time.time() - start_time, 3)
|
| 777 |
-
logger.success(f"Browser {index}: Successfully solved captcha - {COLORS.get('MAGENTA')}{token[:10]}{COLORS.get('RESET')} in {COLORS.get('GREEN')}{elapsed_time}{COLORS.get('RESET')} Seconds")
|
| 778 |
-
await save_result(task_id, "turnstile", {"value": token, "elapsed_time": elapsed_time})
|
| 779 |
-
return
|
| 780 |
-
except Exception as e:
|
| 781 |
-
if self.debug:
|
| 782 |
-
logger.debug(f"Browser {index}: Single token element check failed: {str(e)}")
|
| 783 |
-
else:
|
| 784 |
-
# Если несколько элементов, проверяем все по очереди
|
| 785 |
-
if self.debug:
|
| 786 |
-
logger.debug(f"Browser {index}: Found {count} token elements, checking all")
|
| 787 |
-
|
| 788 |
-
for i in range(count):
|
| 789 |
-
try:
|
| 790 |
-
element_token = await locator.nth(i).input_value(timeout=500)
|
| 791 |
-
if element_token:
|
| 792 |
-
elapsed_time = round(time.time() - start_time, 3)
|
| 793 |
-
logger.success(f"Browser {index}: Successfully solved captcha - {COLORS.get('MAGENTA')}{element_token[:10]}{COLORS.get('RESET')} in {COLORS.get('GREEN')}{elapsed_time}{COLORS.get('RESET')} Seconds")
|
| 794 |
-
await save_result(task_id, "turnstile", {"value": element_token, "elapsed_time": elapsed_time})
|
| 795 |
-
return
|
| 796 |
-
except Exception as e:
|
| 797 |
-
if self.debug:
|
| 798 |
-
logger.debug(f"Browser {index}: Token element {i} check failed: {str(e)}")
|
| 799 |
-
continue
|
| 800 |
-
|
| 801 |
-
if attempt > 2 and attempt % 3 == 0 and click_count < max_clicks:
|
| 802 |
-
click_success = await self._try_click_strategies(page, index)
|
| 803 |
-
click_count += 1
|
| 804 |
-
if click_success and self.debug:
|
| 805 |
-
logger.debug(f"Browser {index}: Click successful (click #{click_count}/{max_clicks})")
|
| 806 |
-
elif not click_success and self.debug:
|
| 807 |
-
logger.debug(f"Browser {index}: All click strategies failed on attempt {attempt + 1} (click #{click_count}/{max_clicks})")
|
| 808 |
-
|
| 809 |
-
# Адаптивное ожидание
|
| 810 |
-
wait_time = min(0.5 + (attempt * 0.05), 2.0)
|
| 811 |
-
await asyncio.sleep(wait_time)
|
| 812 |
-
|
| 813 |
-
if self.debug and attempt % 5 == 0:
|
| 814 |
-
logger.debug(f"Browser {index}: Attempt {attempt + 1}/{max_attempts} - Waiting for token (clicks: {click_count}/{max_clicks})")
|
| 815 |
-
|
| 816 |
-
except Exception as e:
|
| 817 |
-
if self.debug:
|
| 818 |
-
logger.debug(f"Browser {index}: Attempt {attempt + 1} error: {str(e)}")
|
| 819 |
-
continue
|
| 820 |
-
|
| 821 |
-
elapsed_time = round(time.time() - start_time, 3)
|
| 822 |
-
await save_result(task_id, "turnstile", {"value": "CAPTCHA_FAIL", "elapsed_time": elapsed_time})
|
| 823 |
-
if self.debug:
|
| 824 |
-
logger.error(f"Browser {index}: Error solving Turnstile in {COLORS.get('RED')}{elapsed_time}{COLORS.get('RESET')} Seconds")
|
| 825 |
-
except Exception as e:
|
| 826 |
-
elapsed_time = round(time.time() - start_time, 3)
|
| 827 |
-
await save_result(task_id, "turnstile", {"value": "CAPTCHA_FAIL", "elapsed_time": elapsed_time})
|
| 828 |
-
if self.debug:
|
| 829 |
-
logger.error(f"Browser {index}: Error solving Turnstile: {str(e)}")
|
| 830 |
-
finally:
|
| 831 |
-
if self.debug:
|
| 832 |
-
logger.debug(f"Browser {index}: Closing browser context and cleaning up")
|
| 833 |
-
|
| 834 |
-
try:
|
| 835 |
-
await context.close()
|
| 836 |
-
if self.debug:
|
| 837 |
-
logger.debug(f"Browser {index}: Context closed successfully")
|
| 838 |
-
except Exception as e:
|
| 839 |
-
if self.debug:
|
| 840 |
-
logger.warning(f"Browser {index}: Error closing context: {str(e)}")
|
| 841 |
-
|
| 842 |
-
try:
|
| 843 |
-
if hasattr(browser, 'is_connected') and browser.is_connected():
|
| 844 |
-
await self.browser_pool.put((index, browser, browser_config))
|
| 845 |
-
if self.debug:
|
| 846 |
-
logger.debug(f"Browser {index}: Browser returned to pool")
|
| 847 |
-
else:
|
| 848 |
-
if self.debug:
|
| 849 |
-
logger.warning(f"Browser {index}: Browser disconnected, not returning to pool")
|
| 850 |
-
except Exception as e:
|
| 851 |
-
if self.debug:
|
| 852 |
-
logger.warning(f"Browser {index}: Error returning browser to pool: {str(e)}")
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
async def process_turnstile(self):
|
| 860 |
-
"""Handle the /turnstile endpoint requests."""
|
| 861 |
-
url = request.args.get('url')
|
| 862 |
-
sitekey = request.args.get('sitekey')
|
| 863 |
-
action = request.args.get('action')
|
| 864 |
-
cdata = request.args.get('cdata')
|
| 865 |
-
|
| 866 |
-
if not url or not sitekey:
|
| 867 |
-
return jsonify({
|
| 868 |
-
"errorId": 1,
|
| 869 |
-
"errorCode": "ERROR_WRONG_PAGEURL",
|
| 870 |
-
"errorDescription": "Both 'url' and 'sitekey' are required"
|
| 871 |
-
}), 200
|
| 872 |
-
|
| 873 |
-
task_id = str(uuid.uuid4())
|
| 874 |
-
await save_result(task_id, "turnstile", {
|
| 875 |
-
"status": "CAPTCHA_NOT_READY",
|
| 876 |
-
"createTime": int(time.time()),
|
| 877 |
-
"url": url,
|
| 878 |
-
"sitekey": sitekey,
|
| 879 |
-
"action": action,
|
| 880 |
-
"cdata": cdata
|
| 881 |
-
})
|
| 882 |
-
|
| 883 |
-
try:
|
| 884 |
-
asyncio.create_task(self._solve_turnstile(task_id=task_id, url=url, sitekey=sitekey, action=action, cdata=cdata))
|
| 885 |
-
|
| 886 |
-
if self.debug:
|
| 887 |
-
logger.debug(f"Request completed with taskid {task_id}.")
|
| 888 |
-
return jsonify({
|
| 889 |
-
"errorId": 0,
|
| 890 |
-
"taskId": task_id
|
| 891 |
-
}), 200
|
| 892 |
-
except Exception as e:
|
| 893 |
-
logger.error(f"Unexpected error processing request: {str(e)}")
|
| 894 |
-
return jsonify({
|
| 895 |
-
"errorId": 1,
|
| 896 |
-
"errorCode": "ERROR_UNKNOWN",
|
| 897 |
-
"errorDescription": str(e)
|
| 898 |
-
}), 200
|
| 899 |
-
|
| 900 |
-
async def get_result(self):
|
| 901 |
-
"""Return solved data"""
|
| 902 |
-
task_id = request.args.get('id')
|
| 903 |
-
|
| 904 |
-
if not task_id:
|
| 905 |
-
return jsonify({
|
| 906 |
-
"errorId": 1,
|
| 907 |
-
"errorCode": "ERROR_WRONG_CAPTCHA_ID",
|
| 908 |
-
"errorDescription": "Invalid task ID/Request parameter"
|
| 909 |
-
}), 200
|
| 910 |
-
|
| 911 |
-
result = await load_result(task_id)
|
| 912 |
-
if not result:
|
| 913 |
-
return jsonify({
|
| 914 |
-
"errorId": 1,
|
| 915 |
-
"errorCode": "ERROR_CAPTCHA_UNSOLVABLE",
|
| 916 |
-
"errorDescription": "Task not found"
|
| 917 |
-
}), 200
|
| 918 |
-
|
| 919 |
-
if result == "CAPTCHA_NOT_READY" or (isinstance(result, dict) and result.get("status") == "CAPTCHA_NOT_READY"):
|
| 920 |
-
return jsonify({"status": "processing"}), 200
|
| 921 |
-
|
| 922 |
-
if isinstance(result, dict) and result.get("value") == "CAPTCHA_FAIL":
|
| 923 |
-
return jsonify({
|
| 924 |
-
"errorId": 1,
|
| 925 |
-
"errorCode": "ERROR_CAPTCHA_UNSOLVABLE",
|
| 926 |
-
"errorDescription": "Workers could not solve the Captcha"
|
| 927 |
-
}), 200
|
| 928 |
-
|
| 929 |
-
if isinstance(result, dict) and result.get("value") and result.get("value") != "CAPTCHA_FAIL":
|
| 930 |
-
return jsonify({
|
| 931 |
-
"errorId": 0,
|
| 932 |
-
"status": "ready",
|
| 933 |
-
"solution": {
|
| 934 |
-
"token": result["value"]
|
| 935 |
-
}
|
| 936 |
-
}), 200
|
| 937 |
-
else:
|
| 938 |
-
return jsonify({
|
| 939 |
-
"errorId": 1,
|
| 940 |
-
"errorCode": "ERROR_CAPTCHA_UNSOLVABLE",
|
| 941 |
-
"errorDescription": "Workers could not solve the Captcha"
|
| 942 |
-
}), 200
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
@staticmethod
|
| 947 |
-
async def index():
|
| 948 |
-
"""Serve the API documentation page."""
|
| 949 |
-
return """
|
| 950 |
-
<!DOCTYPE html>
|
| 951 |
-
<html lang="en">
|
| 952 |
-
<head>
|
| 953 |
-
<meta charset="UTF-8">
|
| 954 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 955 |
-
<title>Turnstile Solver API</title>
|
| 956 |
-
<script src="https://cdn.tailwindcss.com"></script>
|
| 957 |
-
</head>
|
| 958 |
-
<body class="bg-gray-900 text-gray-200 min-h-screen flex items-center justify-center">
|
| 959 |
-
<div class="bg-gray-800 p-8 rounded-lg shadow-md max-w-2xl w-full border border-red-500">
|
| 960 |
-
<h1 class="text-3xl font-bold mb-6 text-center text-red-500">Welcome to Turnstile Solver API</h1>
|
| 961 |
-
|
| 962 |
-
<p class="mb-4 text-gray-300">To use the turnstile service, send a GET request to
|
| 963 |
-
<code class="bg-red-700 text-white px-2 py-1 rounded">/turnstile</code> with the following query parameters:</p>
|
| 964 |
-
|
| 965 |
-
<ul class="list-disc pl-6 mb-6 text-gray-300">
|
| 966 |
-
<li><strong>url</strong>: The URL where Turnstile is to be validated</li>
|
| 967 |
-
<li><strong>sitekey</strong>: The site key for Turnstile</li>
|
| 968 |
-
</ul>
|
| 969 |
-
|
| 970 |
-
<div class="bg-gray-700 p-4 rounded-lg mb-6 border border-red-500">
|
| 971 |
-
<p class="font-semibold mb-2 text-red-400">Example usage:</p>
|
| 972 |
-
<code class="text-sm break-all text-red-300">/turnstile?url=https://example.com&sitekey=sitekey</code>
|
| 973 |
-
</div>
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
<div class="bg-gray-700 p-4 rounded-lg mb-6">
|
| 977 |
-
<p class="text-gray-200 font-semibold mb-3">📢 Connect with Us</p>
|
| 978 |
-
<div class="space-y-2 text-sm">
|
| 979 |
-
<p class="text-gray-300">
|
| 980 |
-
📢 <strong>Channel:</strong>
|
| 981 |
-
<a href="https://t.me/D3_vin" class="text-red-300 hover:underline">https://t.me/D3_vin</a>
|
| 982 |
-
- Latest updates and releases
|
| 983 |
-
</p>
|
| 984 |
-
<p class="text-gray-300">
|
| 985 |
-
💬 <strong>Chat:</strong>
|
| 986 |
-
<a href="https://t.me/D3vin_chat" class="text-red-300 hover:underline">https://t.me/D3vin_chat</a>
|
| 987 |
-
- Community support and discussions
|
| 988 |
-
</p>
|
| 989 |
-
<p class="text-gray-300">
|
| 990 |
-
📁 <strong>GitHub:</strong>
|
| 991 |
-
<a href="https://github.com/D3-vin" class="text-red-300 hover:underline">https://github.com/D3-vin</a>
|
| 992 |
-
- Source code and development
|
| 993 |
-
</p>
|
| 994 |
-
</div>
|
| 995 |
-
</div>
|
| 996 |
-
</div>
|
| 997 |
-
</body>
|
| 998 |
-
</html>
|
| 999 |
-
"""
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
def parse_args():
|
| 1003 |
-
"""Parse command-line arguments."""
|
| 1004 |
-
parser = argparse.ArgumentParser(description="Turnstile API Server")
|
| 1005 |
-
|
| 1006 |
-
parser.add_argument('--no-headless', action='store_true', help='Run the browser with GUI (disable headless mode). By default, headless mode is enabled.')
|
| 1007 |
-
parser.add_argument('--useragent', type=str, help='User-Agent string (if not specified, random configuration is used)')
|
| 1008 |
-
parser.add_argument('--debug', action='store_true', help='Enable or disable debug mode for additional logging and troubleshooting information (default: False)')
|
| 1009 |
-
parser.add_argument('--browser_type', type=str, default='chromium', help='Specify the browser type for the solver. Supported options: chromium, chrome, msedge, camoufox (default: chromium)')
|
| 1010 |
-
parser.add_argument('--thread', type=int, default=4, help='Set the number of browser threads to use for multi-threaded mode. Increasing this will speed up execution but requires more resources (default: 1)')
|
| 1011 |
-
parser.add_argument('--proxy', action='store_true', help='Enable proxy support for the solver (Default: False)')
|
| 1012 |
-
parser.add_argument('--random', action='store_true', help='Use random User-Agent and Sec-CH-UA configuration from pool')
|
| 1013 |
-
parser.add_argument('--browser', type=str, help='Specify browser name to use (e.g., chrome, firefox)')
|
| 1014 |
-
parser.add_argument('--version', type=str, help='Specify browser version to use (e.g., 139, 141)')
|
| 1015 |
-
parser.add_argument('--host', type=str, default='0.0.0.0', help='Specify the IP address where the API solver runs. (Default: 127.0.0.1)')
|
| 1016 |
-
parser.add_argument('--port', type=str, default='5072', help='Set the port for the API solver to listen on. (Default: 5072)')
|
| 1017 |
-
return parser.parse_args()
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
def create_app(headless: bool, useragent: str, debug: bool, browser_type: str, thread: int, proxy_support: bool, use_random_config: bool, browser_name: str, browser_version: str) -> Quart:
|
| 1021 |
-
server = TurnstileAPIServer(headless=headless, useragent=useragent, debug=debug, browser_type=browser_type, thread=thread, proxy_support=proxy_support, use_random_config=use_random_config, browser_name=browser_name, browser_version=browser_version)
|
| 1022 |
-
return server.app
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
if __name__ == '__main__':
|
| 1026 |
-
args = parse_args()
|
| 1027 |
-
browser_types = [
|
| 1028 |
-
'chromium',
|
| 1029 |
-
'chrome',
|
| 1030 |
-
'msedge',
|
| 1031 |
-
'camoufox',
|
| 1032 |
-
]
|
| 1033 |
-
if args.browser_type not in browser_types:
|
| 1034 |
-
logger.error(f"Unknown browser type: {COLORS.get('RED')}{args.browser_type}{COLORS.get('RESET')} Available browser types: {browser_types}")
|
| 1035 |
-
else:
|
| 1036 |
-
app = create_app(
|
| 1037 |
-
headless=not args.no_headless,
|
| 1038 |
-
debug=args.debug,
|
| 1039 |
-
useragent=args.useragent,
|
| 1040 |
-
browser_type=args.browser_type,
|
| 1041 |
-
thread=args.thread,
|
| 1042 |
-
proxy_support=args.proxy,
|
| 1043 |
-
use_random_config=args.random,
|
| 1044 |
-
browser_name=args.browser,
|
| 1045 |
-
browser_version=args.version
|
| 1046 |
-
)
|
| 1047 |
-
app.run(host=args.host, port=int(args.port))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/turnstile_solver/browser_configs.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
import random
|
| 2 |
-
|
| 3 |
-
class browser_config:
|
| 4 |
-
@staticmethod
|
| 5 |
-
def get_random_browser_config(browser_type):
|
| 6 |
-
# 返回: 浏览器名, 版本, User-Agent, Sec-CH-UA
|
| 7 |
-
versions = ["120.0.0.0", "121.0.0.0", "122.0.0.0", "124.0.0.0"]
|
| 8 |
-
ver = random.choice(versions)
|
| 9 |
-
ua = f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{ver} Safari/537.36"
|
| 10 |
-
sec_ch_ua = f'"Not(A:Brand";v="99", "Google Chrome";v="{ver.split(".")[0]}", "Chromium";v="{ver.split(".")[0]}"'
|
| 11 |
-
return "chrome", ver, ua, sec_ch_ua
|
| 12 |
-
|
| 13 |
-
@staticmethod
|
| 14 |
-
def get_browser_config(name, version):
|
| 15 |
-
ua = f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{version} Safari/537.36"
|
| 16 |
-
sec_ch_ua = f'"Google Chrome";v="{version}", "Chromium";v="{version}"'
|
| 17 |
-
return ua, sec_ch_ua
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/turnstile_solver/db_results.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
import time
|
| 2 |
-
import asyncio
|
| 3 |
-
|
| 4 |
-
# 内存数据库,用于临时存储验证码结果
|
| 5 |
-
results_db = {}
|
| 6 |
-
|
| 7 |
-
async def init_db():
|
| 8 |
-
print("[系统] 结果数据库初始化成功 (内存模式)")
|
| 9 |
-
|
| 10 |
-
async def save_result(task_id, task_type, data):
|
| 11 |
-
# 存储结果,如果 data 是字典则存入,否则构造字典
|
| 12 |
-
results_db[task_id] = data
|
| 13 |
-
print(f"[系统] 任务 {task_id} 状态更新: {data.get('value', '正在处理')}")
|
| 14 |
-
|
| 15 |
-
async def load_result(task_id):
|
| 16 |
-
return results_db.get(task_id)
|
| 17 |
-
|
| 18 |
-
async def cleanup_old_results(days_old=7):
|
| 19 |
-
# 简单的清理逻辑
|
| 20 |
-
now = time.time()
|
| 21 |
-
to_delete = []
|
| 22 |
-
for tid, res in results_db.items():
|
| 23 |
-
if isinstance(res, dict) and now - res.get('createTime', now) > days_old * 86400:
|
| 24 |
-
to_delete.append(tid)
|
| 25 |
-
for tid in to_delete:
|
| 26 |
-
del results_db[tid]
|
| 27 |
-
return len(to_delete)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/wait_for_storage.py
DELETED
|
@@ -1,85 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import socket
|
| 3 |
-
import sys
|
| 4 |
-
import time
|
| 5 |
-
from urllib.parse import urlparse
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
DEFAULT_PORTS: dict[str, int] = {
|
| 9 |
-
"redis": 6379,
|
| 10 |
-
"mysql": 3306,
|
| 11 |
-
"pgsql": 5432,
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
def _log(msg: str) -> None:
|
| 16 |
-
print(f"[wait_for_storage] {msg}", flush=True)
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def _as_int(value: str | None, default: int) -> int:
|
| 20 |
-
try:
|
| 21 |
-
return int(value) if value is not None else default
|
| 22 |
-
except Exception:
|
| 23 |
-
return default
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def _get_target(storage_type: str, storage_url: str) -> tuple[str, int] | None:
|
| 27 |
-
if not storage_url:
|
| 28 |
-
return None
|
| 29 |
-
|
| 30 |
-
parsed = urlparse(storage_url)
|
| 31 |
-
host = parsed.hostname
|
| 32 |
-
if not host:
|
| 33 |
-
return None
|
| 34 |
-
|
| 35 |
-
port = parsed.port
|
| 36 |
-
if port is None:
|
| 37 |
-
port = DEFAULT_PORTS.get(storage_type)
|
| 38 |
-
|
| 39 |
-
if port is None:
|
| 40 |
-
return None
|
| 41 |
-
|
| 42 |
-
return host, int(port)
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def main() -> int:
|
| 46 |
-
storage_type = (os.getenv("SERVER_STORAGE_TYPE", "local") or "local").lower().strip()
|
| 47 |
-
if storage_type in {"", "local"}:
|
| 48 |
-
return 0
|
| 49 |
-
|
| 50 |
-
storage_url = (os.getenv("SERVER_STORAGE_URL", "") or "").strip()
|
| 51 |
-
target = _get_target(storage_type, storage_url)
|
| 52 |
-
if target is None:
|
| 53 |
-
_log(
|
| 54 |
-
f"skip: unable to parse host/port for SERVER_STORAGE_TYPE={storage_type!r} from SERVER_STORAGE_URL",
|
| 55 |
-
)
|
| 56 |
-
return 1
|
| 57 |
-
|
| 58 |
-
host, port = target
|
| 59 |
-
timeout_s = _as_int(os.getenv("STORAGE_WAIT_TIMEOUT"), 60)
|
| 60 |
-
interval_s = max(0.1, float(os.getenv("STORAGE_WAIT_INTERVAL", "0.5") or "0.5"))
|
| 61 |
-
|
| 62 |
-
_log(f"waiting for {storage_type} at {host}:{port} (timeout={timeout_s}s)")
|
| 63 |
-
|
| 64 |
-
deadline = time.monotonic() + max(1, timeout_s)
|
| 65 |
-
last_log_at = 0.0
|
| 66 |
-
|
| 67 |
-
while time.monotonic() < deadline:
|
| 68 |
-
try:
|
| 69 |
-
with socket.create_connection((host, port), timeout=2):
|
| 70 |
-
_log("ready")
|
| 71 |
-
return 0
|
| 72 |
-
except OSError as e:
|
| 73 |
-
now = time.monotonic()
|
| 74 |
-
if now - last_log_at >= 3:
|
| 75 |
-
_log(f"not ready yet: {e.__class__.__name__}: {e}")
|
| 76 |
-
last_log_at = now
|
| 77 |
-
time.sleep(interval_s)
|
| 78 |
-
|
| 79 |
-
_log("timeout: storage is still not reachable")
|
| 80 |
-
return 1
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
if __name__ == "__main__":
|
| 84 |
-
raise SystemExit(main())
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|