المرجع

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

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

هذا المستوى من الدقة يجعل CDP خياراً عملياً لفرق ضمان الجودة التي تختبر تدفقات تسجيل الدخول والنماذج المحمية بـ CAPTCHA على بيئات staging مأذونة، حيث يهم التحكم اللحظي في كل طلب واستجابة.

تنقسم الدورة إلى أربع مراحل تقابل كلاً منها أوامر CDP محددة:

  1. الاتصال: افتح جلسة WebSocket من عنوان التصحيح المعلن في /json/list، واربطها بالتبويب النشط في المتصفح.
  2. اكتشاف مفتاح الموقع: نفّذ Runtime.evaluate لقراءة السمة data-sitekey من عناصر الصفحة وتحديد نوع CAPTCHA.
  3. الإرسال إلى CaptchaAI: ابعث المهمة إلى نقطة النهاية in.php، ثم استطلع النتيجة دورياً من res.php حتى تكتمل المعالجة.
  4. حقن التوكن: أعد كتابة حقل الاستجابة في الصفحة وأطلق دالة رد النداء لإكمال التحقق دون تدخل يدوي.

متى يناسبك تحكم CDP المباشر؟

ليس كل مشروع أتمتة يستدعي هذا المستوى من التحكم منخفض المستوى، لكن CDP يثبت جدواه تحديداً في الحالات التالية:

  • اختبار الانحدار الليلي على بيئات staging تحوي نماذج محمية بـ reCAPTCHA v2 أو Cloudflare Turnstile.
  • تشخيص تدفق التحقق حين تحتاج إلى رصد كل طلب شبكة يمرّ عبر عنصر CAPTCHA وتسجيله لحظياً.
  • خطوط CI بلا واجهة رسومية التي تشغّل Chrome في وضع headless على خوادم الاختبار.
  • التكامل مع أدوات مبنية على CDP مثل Puppeteer، عبر مشاركة الجلسة نفسها دون طبقة وسيطة إضافية.

لماذا تختار CDP بدلاً من WebDriver؟

الميزة WebDriver CDP
اعتراض طلبات الشبكة يتطلب أداة Selenium Wire مدمج عبر Fetch.requestPaused
حقن JavaScript عبر executeScript عبر Runtime.evaluate
العبء على الأداء متوسط إلى مرتفع منخفض
بروتوكول الاتصال HTTP + JSON Wire WebSocket لحظي

باختصار، يتعامل WebDriver مع المتصفح من الخارج عبر طبقة وسيطة، بينما يخاطب CDP محرّك Chrome مباشرة — فتحصل على استجابة أسرع وتحكّم أدق عندما تحتاج إلى مراقبة كل طلب CAPTCHA أثناء الاختبار.


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

الاتصال بـ Chrome

# Launch Chrome with remote debugging
chrome --remote-debugging-port=9222 --no-first-run --no-default-browser-check

أولاً، شغّل Chrome مع منفذ التصحيح عن بُعد كما في الأمر أعلاه، ثم يتولى عميل خفيف إدارة جلسة WebSocket وتوجيه الرسائل.

بناء عميل CDP خفيف

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);
  }
}

يبني هذا الكلاس قناة اتصال دائمة عبر WebSocket: يقرأ عنوان التصحيح من Chrome، يفتح الجلسة، ثم يوجّه كل رسالة واردة إما إلى دالة رد النداء المنتظرة أو إلى مستمع الحدث المناسب.


ربط CaptchaAI بجلسة CDP

يجمع الكلاس التالي بين التحكم في المتصفح عبر CDP ونداءات CaptchaAI في وحدة واحدة تديّر دورة الحل كاملة.

تجميع الحل والحقن في كلاس واحد

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));
  }
}

يوحّد CDPCaptchaSolver بين طبقتين: جلسة CDP للتحكم في الصفحة، ونقطتا النهاية in.php وres.php لدى CaptchaAI. لاحظ الفرق في الحمولة — يستخدم reCAPTCHA الحقل googlekey مع الأسلوب userrecaptcha، بينما يستخدم Turnstile الحقل sitekey مع الأسلوب turnstile. الدالة solveCaptcha تستطلع النتيجة دورياً كل خمس ثوانٍ حتى تنتهي المعالجة أو تنقضي المهلة.


اعتراض طلبات CAPTCHA على مستوى الشبكة

تفعيل نطاق Fetch لإيقاف الطلبات

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 });
  });
}

يوقف Fetch.enable طلبات CAPTCHA المطابقة للأنماط المحددة قبل إرسالها، فتستطيع تسجيلها أو تشخيصها ثم متابعتها عبر Fetch.continueRequest. هذه نقطة مراقبة قيّمة أثناء تصحيح تدفق التحقق داخل بيئة الاختبار.


تنفيذ CDP في Python

نقل نفس المنطق إلى asyncio

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")

تنقل نسخة Python المنطق نفسه إلى بيئة asyncio: تتصل بالجلسة، تكتشف مفتاح الموقع، تحل عبر CaptchaAI، ثم تحقن التوكن — وهو ما يناسب خطوط الاختبار المبنية على aiohttp وrequests.


سيناريو كامل: من الاكتشاف إلى حقن التوكن

// 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);

مثال عملي: يدير فريق ضمان جودة في منصة تجارة إلكترونية بالخليج بيئة staging مطابقة للإنتاج ومحمية بـ reCAPTCHA v2 على صفحة تسجيل الدخول. بدلاً من الإدخال اليدوي المتكرر في كل دورة اختبار، يشغّل الفريق جلسة CDP تكتشف مفتاح الموقع تلقائياً، ترسل المهمة إلى CaptchaAI، وتحقن التوكن — فتكتمل دورة الاختبار الليلية دون تدخل بشري، مع سجل كامل لكل طلب شبكة اعتُرض أثناء التشغيل.


أفضل ممارسات التشغيل الآمن

قبل تشغيل أي جلسة CDP على نطاق واسع، ثبّت الضوابط التالية لتبقى الأتمتة داخل الإطار المأذون وسهلة الصيانة:

  • شغّل الاختبارات على بيئات staging تملكها أو مفوّض لك اختبارها، لا على مواقع إنتاج تابعة لجهات أخرى.
  • خزّن مفتاح CaptchaAI في متغيّر بيئة، ولا تضعه مباشرة داخل السكربت أو في سجلات التشغيل.
  • سجّل كل طلب شبكة اعتُرض في مسار تدقيق قابل للمراجعة، فهذا يسرّع تشخيص الأعطال لاحقاً.
  • راقب الرصيد ومعدل الحل بمرور الوقت لتضبط عدد محاولات الاستطلاع والمهلة بما يناسب حجم الحِمل.

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


حل المشكلات الشائعة في CDP

المشكلة السبب الإجراء
تعذّر الاتصال بـ Chrome لم يُفعّل التصحيح عن بُعد شغّل Chrome بالوسيط --remote-debugging-port=9222
انقطاع اتصال WebSocket تعطّل المتصفح أو أُغلقت علامة التبويب أضف منطق إعادة اتصال تلقائي
فشل Runtime.evaluate الصفحة لم تُحمّل بعد انتظر حدث Page.loadEventFired قبل التنفيذ
انتهاء المهلة أثناء الحل زمن حل أطول من المتوقع تحت الحِمل ارفع عدد محاولات الاستطلاع أو زد المهلة بين الطلبات
فشل حقن التوكن عنصر CAPTCHA داخل Shadow DOM استخدم DOM.describeNode لاجتياز جذور الظل (shadow roots)

أسئلة شائعة حول CDP وCaptchaAI

كيف أتعامل مع CAPTCHA داخل Shadow DOM عبر CDP؟

عندما يكون عنصر CAPTCHA داخل Shadow DOM، لن تصل إليه استعلامات document.querySelector العادية. استخدم DOM.describeNode مع تفعيل خيار pierce لاجتياز جذور الظل والوصول إلى العنصر الهدف قبل قراءة مفتاح الموقع أو حقن التوكن.

ما الفرق بين Fetch.enable وNetwork.enable لاعتراض الطلبات؟

يتيح Network.enable مراقبة الطلبات والاستجابات وتسجيلها فقط، بينما يمنحك Fetch.enable القدرة على إيقاف الطلب مؤقتاً عبر Fetch.requestPaused ثم تعديله أو متابعته. استخدم Fetch حين تحتاج إلى التدخل في الطلب، وNetwork حين يكفيك الرصد.

هل يمكن تشغيل جلسة CDP في وضع headless على خادم اختبار؟

نعم. أضف الوسيط --headless=new عند تشغيل Chrome مع --remote-debugging-port، وستعمل أوامر CDP نفسها دون واجهة رسومية. هذا مناسب لخطوط CI التي تشغّل اختبارات الليل على خوادم بلا شاشة.

هل يتعارض CDP مع سياسات الحماية على الموقع؟

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


روابط ذات صلة


جاهز للبدء؟ أنشئ مفتاح CaptchaAI الخاص بك واربطه بجلسة CDP لأتمتة اختبار CAPTCHA على مستوى البروتوكول.

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