المرجع

بروتوكول Chrome DevTools + CaptchaAI: أتمتة اختبار CAPTCHA منخفض المستوى

يمنحك بروتوكول Chrome DevTools (CDP) تحكمًا مباشرًا في Chrome على مستوى البروتوكول - بدون WebDriver، ولا توجد طبقات تجريد إضافية. بالنسبة لأتمتة اختبار CAPTCHA في بيئات الاختبار المُملوكة، يعني هذا تحكمًا دقيقًا في طلبات الشبكة وتنفيذ الصفحة وسلوك المتصفح ضمن اختبار موثّق ومأذون.


لماذا CDP على WebDriver؟

ميزة برنامج تشغيل الويب CDP
اعتراض الشبكة يتطلب سلك السيلينيوم مدمج (Fetch.requestPaused)
حقن JavaScript executeScript Runtime.evaluate
الأداء العام متوسطة عالية منخفض
دعم البروتوكول سلك HTTP + JSON WebSocket (في الوقت الحقيقي)

اتصال CDP المباشر (Node.js)

اتصل بمتصفح Chrome

# Launch Chrome with remote debugging
chrome --remote-debugging-port=9222 --no-first-run --no-default-browser-check
const WebSocket = require("ws");
const http = require("http");

class CDPClient {
  constructor() {
    this.ws = null;
    this.id = 0;
    this.callbacks = new Map();
    this.eventHandlers = new Map();
  }

  async connect(port = 9222) {
    // Get WebSocket URL from Chrome
    const targets = await this.httpGet(
      `http://127.0.0.1:${port}/json/list`
    );
    const target = targets.find((t) => t.type === "page");

    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(target.webSocketDebuggerUrl);
      this.ws.on("open", () => resolve());
      this.ws.on("error", reject);
      this.ws.on("message", (data) => this.handleMessage(JSON.parse(data)));
    });
  }

  httpGet(url) {
    return new Promise((resolve, reject) => {
      http.get(url, (res) => {
        let body = "";
        res.on("data", (c) => (body += c));
        res.on("end", () => resolve(JSON.parse(body)));
      }).on("error", reject);
    });
  }

  handleMessage(msg) {
    if (msg.id && this.callbacks.has(msg.id)) {
      this.callbacks.get(msg.id)(msg);
      this.callbacks.delete(msg.id);
    }
    if (msg.method && this.eventHandlers.has(msg.method)) {
      for (const handler of this.eventHandlers.get(msg.method)) {
        handler(msg.params);
      }
    }
  }

  send(method, params = {}) {
    return new Promise((resolve) => {
      const id = ++this.id;
      this.callbacks.set(id, resolve);
      this.ws.send(JSON.stringify({ id, method, params }));
    });
  }

  on(method, handler) {
    if (!this.eventHandlers.has(method)) {
      this.eventHandlers.set(method, []);
    }
    this.eventHandlers.get(method).push(handler);
  }
}

تكامل CaptchaAI + CDP

const https = require("https");

class CDPCaptchaSolver {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.cdp = new CDPClient();
    this.API = "https://ocr.captchaai.com";
  }

  async init(port = 9222) {
    await this.cdp.connect(port);

    // Enable required domains
    await this.cdp.send("Page.enable");
    await this.cdp.send("Runtime.enable");
    await this.cdp.send("Network.enable");
    await this.cdp.send("DOM.enable");
  }

  async navigate(url) {
    const result = await this.cdp.send("Page.navigate", { url });
    await this.waitForLoad();
    return result;
  }

  async waitForLoad() {
    return new Promise((resolve) => {
      this.cdp.on("Page.loadEventFired", () => resolve());
    });
  }

  async detectSitekey() {
    const result = await this.cdp.send("Runtime.evaluate", {
      expression: `
        (() => {
          // reCAPTCHA
          const recaptcha = document.querySelector('[data-sitekey]');
          if (recaptcha) {
            return {
              type: 'recaptcha_v2',
              sitekey: recaptcha.getAttribute('data-sitekey'),
            };
          }

          // Turnstile
          const turnstile = document.querySelector('.cf-turnstile[data-sitekey]');
          if (turnstile) {
            return {
              type: 'turnstile',
              sitekey: turnstile.getAttribute('data-sitekey'),
            };
          }

          // reCAPTCHA v3 (script-based)
          const scripts = document.querySelectorAll('script[src*="recaptcha"]');
          for (const s of scripts) {
            const match = s.src.match(/render=([\\w-]+)/);
            if (match && match[1] !== 'explicit') {
              return { type: 'recaptcha_v3', sitekey: match[1] };
            }
          }

          return null;
        })()
      `,
      returnByValue: true,
    });

    return result.result?.value || null;
  }

  async solveCaptcha(siteUrl, sitekey, type = "recaptcha_v2") {
    const submitData = {
      key: this.apiKey,
      pageurl: siteUrl,
      json: "1",
    };

    if (type === "turnstile") {
      submitData.method = "turnstile";
      submitData.sitekey = sitekey;
    } else {
      submitData.method = "userrecaptcha";
      submitData.googlekey = sitekey;
    }

    const submitResp = await this.httpPost(
      `${this.API}/in.php`,
      submitData
    );

    if (submitResp.status !== 1) {
      throw new Error(`Submit: ${submitResp.request}`);
    }

    const taskId = submitResp.request;

    for (let i = 0; i < 60; i++) {
      await this.sleep(5000);

      const params = new URLSearchParams({
        key: this.apiKey,
        action: "get",
        id: taskId,
        json: "1",
      });

      const result = await this.httpGet(
        `${this.API}/res.php?${params}`
      );

      if (result.request === "CAPCHA_NOT_READY") continue;
      if (result.status !== 1) throw new Error(`Solve: ${result.request}`);
      return result.request;
    }

    throw new Error("Timeout");
  }

  async injectToken(token, type = "recaptcha_v2") {
    if (type === "turnstile") {
      await this.cdp.send("Runtime.evaluate", {
        expression: `
          const input = document.querySelector('input[name="cf-turnstile-response"]');
          if (input) {
            input.value = '${token}';
            input.dispatchEvent(new Event('change', { bubbles: true }));
          }
        `,
      });
    } else {
      await this.cdp.send("Runtime.evaluate", {
        expression: `
          // Set response textarea
          const textarea = document.querySelector('#g-recaptcha-response');
          if (textarea) {
            textarea.style.display = 'block';
            textarea.value = '${token}';
          }

          // Set all hidden fields
          document.querySelectorAll('[name="g-recaptcha-response"]')
            .forEach(el => { el.value = '${token}'; });

          // Trigger callback
          if (typeof ___grecaptcha_cfg !== 'undefined') {
            const clients = ___grecaptcha_cfg.clients;
            for (const key in clients) {
              const client = clients[key];
              for (const prop in client) {
                const val = client[prop];
                if (val && typeof val === 'object') {
                  for (const p in val) {
                    if (typeof val[p]?.callback === 'function') {
                      val[p].callback('${token}');
                    }
                  }
                }
              }
            }
          }
        `,
      });
    }
  }

  // Full workflow
  async solveOnPage(url) {
    await this.navigate(url);
    await this.sleep(2000);

    const captcha = await this.detectSitekey();
    if (!captcha) {
      console.log("No CAPTCHA detected");
      return null;
    }

    console.log(`Detected: ${captcha.type} (${captcha.sitekey})`);
    const token = await this.solveCaptcha(url, captcha.sitekey, captcha.type);
    await this.injectToken(token, captcha.type);
    console.log("Token injected");
    return token;
  }

  // HTTP helpers
  httpPost(url, data) {
    return new Promise((resolve, reject) => {
      const params = new URLSearchParams(data).toString();
      const u = new URL(url);
      const req = https.request({
        hostname: u.hostname, path: u.pathname,
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
      }, (res) => {
        let body = "";
        res.on("data", (c) => (body += c));
        res.on("end", () => resolve(JSON.parse(body)));
      });
      req.on("error", reject);
      req.write(params);
      req.end();
    });
  }

  httpGet(url) {
    return new Promise((resolve, reject) => {
      https.get(url, (res) => {
        let body = "";
        res.on("data", (c) => (body += c));
        res.on("end", () => resolve(JSON.parse(body)));
      }).on("error", reject);
    });
  }

  sleep(ms) {
    return new Promise((r) => setTimeout(r, ms));
  }
}

اعتراض الشبكة مع CDP

async function interceptCaptchaRequests(solver) {
  // Enable Fetch domain for request interception
  await solver.cdp.send("Fetch.enable", {
    patterns: [
      { urlPattern: "*recaptcha*", requestStage: "Request" },
      { urlPattern: "*turnstile*", requestStage: "Request" },
      { urlPattern: "*challenges.cloudflare.com*", requestStage: "Request" },
    ],
  });

  solver.cdp.on("Fetch.requestPaused", async (params) => {
    const { requestId, request } = params;

    console.log(`Intercepted: ${request.method} ${request.url}`);

    // Log CAPTCHA-related requests for debugging
    if (request.url.includes("userverify") || request.url.includes("reload")) {
      console.log("CAPTCHA verification request detected");
    }

    // Continue the request
    await solver.cdp.send("Fetch.continueRequest", { requestId });
  });
}

تكامل بايثون CDP

import asyncio
import aiohttp
import json

class CDPCaptchaSolver:
    CAPTCHAAI_URL = "https://ocr.captchaai.com"

    def __init__(self, api_key, cdp_port=9222):
        self.api_key = api_key
        self.cdp_port = cdp_port
        self.ws = None
        self.msg_id = 0

    async def connect(self):
        async with aiohttp.ClientSession() as session:
            async with session.get(
                f"http://127.0.0.1:{self.cdp_port}/json/list"
            ) as resp:
                targets = await resp.json()

        target = next(t for t in targets if t["type"] == "page")
        self.ws = await asyncio.get_event_loop().create_connection(
            lambda: CDPProtocol(self),
            target["webSocketDebuggerUrl"],
        )

    async def send(self, method, params=None):
        self.msg_id += 1
        msg = {"id": self.msg_id, "method": method, "params": params or {}}
        self.ws.send(json.dumps(msg))
        # Wait for response (simplified)
        return await self._wait_response(self.msg_id)

    async def solve_and_inject(self, url):
        await self.send("Page.navigate", {"url": url})
        await asyncio.sleep(3)

        # Detect sitekey via Runtime.evaluate
        result = await self.send("Runtime.evaluate", {
            "expression": "document.querySelector('[data-sitekey]')?.getAttribute('data-sitekey')",
            "returnByValue": True,
        })

        sitekey = result.get("result", {}).get("value")
        if not sitekey:
            return None

        # Solve via CaptchaAI
        token = await self._solve_recaptcha(url, sitekey)

        # Inject
        await self.send("Runtime.evaluate", {
            "expression": f"""
                document.querySelector('#g-recaptcha-response').value = '{token}';
                document.querySelectorAll('[name="g-recaptcha-response"]')
                    .forEach(el => {{ el.value = '{token}'; }});
            """,
        })

        return token

    async def _solve_recaptcha(self, site_url, sitekey):
        import requests

        resp = requests.post(f"{self.CAPTCHAAI_URL}/in.php", data={
            "key": self.api_key, "method": "userrecaptcha",
            "googlekey": sitekey, "pageurl": site_url, "json": 1,
        })
        task_id = resp.json()["request"]

        for _ in range(60):
            await asyncio.sleep(5)
            resp = requests.get(f"{self.CAPTCHAAI_URL}/res.php", params={
                "key": self.api_key, "action": "get",
                "id": task_id, "json": 1,
            })
            data = resp.json()
            if data["request"] == "CAPCHA_NOT_READY":
                continue
            if data["status"] == 1:
                return data["request"]

        raise TimeoutError("CAPTCHA solve timeout")

مثال الاستخدام

// Full workflow
async function main() {
  const solver = new CDPCaptchaSolver("YOUR_API_KEY");
  await solver.init(9222);

  // Enable network monitoring
  await interceptCaptchaRequests(solver);

  // Solve CAPTCHA on target page
  const token = await solver.solveOnPage("https://example.com/login");

  if (token) {
    // Submit form
    await solver.cdp.send("Runtime.evaluate", {
      expression: `document.querySelector('form').submit()`,
    });
  }
}

main().catch(console.error);

استكشاف الأخطاء وإصلاحها

المشكلة السبب الإجراء
لا يمكن الاتصال بمتصفح Chrome لم يتم تمكين تصحيح الأخطاء عن بعد ابدأ باستخدام --remote-debugging-port=9222
يتم إغلاق WebSocket تعطل Chrome أو تم إغلاق علامة التبويب إضافة منطق إعادة الاتصال
فشل Runtime.evaluate لم يتم تحميل الصفحة انتظر Page.loadEventFired
فشل حقن الرمز المميز في المرحلة الأولى CAPTCHA داخل Shadow DOM استخدم DOM.describeNode لاجتياز جذور الظل
فشل حقن الرمز المميز يحتوي Shadow DOM على اختبار CAPTCHA استخدم DOM.describeNode لاجتياز جذور الظل

الأسئلة الشائعة

هل استخدام CDP أصعب من WebDriver؟

يمنح CDP وصولًا أدق إلى أحداث الصفحة والشبكة، لكنه يتطلب فهمًا أوثق لدورة حياة التبويب. استخدمه عندما تحتاج إلى تشخيص تفصيلي داخل بيئة مملوكة.

هل يمكنني استخدام CDP مع Puppeteer؟

نعم - يستخدم Puppeteer CDP تحت الغطاء. يمكنك الوصول إلى جلسة CDP مباشرة عبر page._client() أو page.createCDPSession().

هل يعمل CDP مع فايرفوكس؟

يتمتع Firefox بدعم CDP جزئي. للحصول على CDP الكامل، استخدم المتصفحات المستندة إلى Chromium.

هل يغيّر CDP قرارات الحماية على الموقع؟

لا. CDP مجرد واجهة تحكم وتشخيص. إذا كان الموقع يفرض تحديات معينة، فاختبرها داخل بيئة تملكها أو بيئة staging مفوضة، ولا تتعامل مع CDP على أنه وسيلة لتغيير سياسات الحماية.


أدلة ذات صلة


الحصول على التحكم على مستوى البروتوكول في أتمتة اختبار CAPTCHA —احصل على مفتاح CaptchaAI الخاص بكوالتكامل مع بروتوكول Chrome DevTools.

التعليقات غير مفعّلة لهذا المقال.