استكشاف الأخطاء

أخطاء التحقق من نطاق reCAPTCHA وكيفية إصلاحها

إذا كان الخادم يرفض رمز reCAPTCHA رغم نجاح الحل، فالسبب غالباً ليس الرمز نفسه بل النطاق. يربط reCAPTCHA كل رمز باسم المضيف الذي تولّد عليه، فإذا أُرسل إلى نطاق يختلف عن قيمة pageurl التي مرّرتها للحل، يرفضه الخادم بصمت — تبدو الاستجابة سليمة لكن التحقق يفشل. نبدأ من أكثر الأسباب تكراراً — عدم تطابق pageurl — ثم نمرّ على كل خطأ نطاق وطريقة إصلاحه في الأتمتة.


كيف يربط reCAPTCHA الرمز بالنطاق

يمرّ التحقق من النطاق بسلسلة ثابتة، وأي حلقة تنكسر فيها تنتج خطأ نطاق:

  1. يسجّل مالك الموقع النطاقات المسموح بها لمفتاح الموقع.
  2. يولّد المتصفح رمزاً يحمل اسم المضيف بداخله عند ظهور الأداة.
  3. يتحقق الخادم من الرمز عبر siteverify فيقارن اسم المضيف بالنطاقات المسجّلة.
Site owner registers reCAPTCHA → adds allowed domains (example.com, www.example.com)
    ↓
reCAPTCHA widget loads on example.com → matches allowed domain ✓
    ↓
Token generated with embedded hostname
    ↓
Server validates token via siteverify API
    ↓
Google checks: Does token hostname match allowed domains?
    ├─ YES → { "success": true, "hostname": "example.com" }
    └─ NO  → { "success": false, error or hostname mismatch }

أين يُفحص النطاق بالضبط

يجري الفحص عند ثلاث نقاط، لكل منها دور:

نقطة الفحص ما الذي يُتحقق منه
جانب العميل (المتصفح) لا تُحمّل الأداة إلا على النطاقات المسموح بها — فحص اختياري يمكن لمالك الموقع تعطيله
توليد الرمز اسم المضيف المضمَّن في الرمز يطابق أصل الصفحة التي ظهرت فيها الأداة
تحقّق الخادم تُعيد siteverify اسم المضيف، وعلى خادم الموقع أن يقرر قبوله أو رفضه

أبرز أخطاء التحقق من النطاق ومعناها

الخطأ الأول: عدم تطابق اسم المضيف في استجابة siteverify

{
    "success": true,
    "hostname": "subdomain.example.com",
    "challenge_ts": "2025-01-15T10:30:00Z"
}

الرمز صالح فعلاً (success: true)، لكن حقل hostname يشير إلى نطاق غير متوقَّع، وبعض تطبيقات التحقق ترفض هذه الحالة صراحةً:

# Server-side validation that checks hostname
def validate_token(token, secret_key, expected_hostname):
    result = requests.post(
        "https://www.google.com/recaptcha/api/siteverify",
        data={"secret": secret_key, "response": token},
    ).json()

    if not result.get("success"):
        return False

    # This check causes failures when hostnames don't match
    if result.get("hostname") != expected_hostname:
        return False  # Domain mismatch!

    return True

متى يحدث ذلك؟

  • حُلّ الرمز على www.example.com بينما يجري التحقق منه على example.com
  • حُلّ الرمز على staging.example.com ثم أُرسل للتحقق على نطاق الإنتاج example.com
  • يغيّر خادم وسيط أو شبكة CDN اسم المضيف الظاهر

الإصلاح

اجعل قيمة pageurl في طلب الحل مطابقة تماماً للنطاق الذي سيُرسَل إليه الرمز لاحقاً.

الخطأ الثاني: الأداة ترفض الظهور من الأساس

لا تظهر أداة reCAPTCHA وتطبع رسالة خطأ بدلاً من ذلك:

ERROR: Invalid domain for site key

متى يحدث ذلك؟

  • النطاقات المسموح بها لمفتاح الموقع لا تتضمن نطاق الصفحة الحالية
  • تحميل الأداة من localhost أو عبر بروتوكول file://
  • استخدام عنوان IP بدل اسم النطاق

الإصلاح في الأتمتة

هذا خطأ في إعدادات مالك الموقع لا في كودك أنت. من جانب الحل، يكفي أن تمرّر pageurl صحيحاً يقع ضمن النطاقات المسموح بها.

الخطأ الثالث: رمز صحيح لكنه مرفوض

{
    "success": false,
    "error-codes": ["invalid-input-response"]
}

هنا success: false مع invalid-input-response، أي أن الرمز تولّد لنطاق يختلف عن نطاق التحقق.

السبب الأكثر شيوعاً في الأتمتة

قيمة pageurl المُرسَلة إلى الخدمة لا تطابق النطاق المستهدف الفعلي:

# WRONG: pageurl doesn't match actual target
submit = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": sitekey,
    "pageurl": "https://example.com/login",  # ← Must match actual domain
    "json": 1,
})

# But submitting token to:
requests.post("https://app.example.com/login", ...)  # Different subdomain!

جدول سريع لتشخيص الأعطال

قبل الإصلاحات التفصيلية، ابدأ من العرَض ثم اتبع التشخيص المقابل له:

العرَض السبب المرجّح التشخيص الإصلاح
الرمز مرفوض دائماً pageurl لا يطابق النطاق المستهدف قارن pageurl المُرسَل للحل بنطاق الإرسال الفعلي حدّث pageurl ليطابق النطاق
ينجح على www ويفشل على غير www (أو العكس) اختلاف صيغة النطاق افحص سلوك إعادة التوجيه استخدم الصيغة التي يعتمدها الموقع فعلاً (بتتبّع إعادة التوجيه)
ينجح أحياناً ويفشل أحياناً شبكة CDN أو موازن تحميل يقدّم نطاقات مختلفة تحقق إن كان النطاق يتغيّر بين طلب وآخر ثبّت نطاقاً واحداً من سلسلة إعادة التوجيه
ينجح في المتصفح ويفشل في السكربت السكربت يرسل من أصل مختلف قارن شريط عنوان المتصفح بقيمة pageurl في السكربت طابق النطاق النهائي الذي يظهر في المتصفح
رمز Enterprise مرفوض مشروع خاطئ أو ربط نطاق غير صحيح تأكد أن مفتاح موقع Enterprise مربوط بالنطاق راجع إعدادات النطاق في لوحة Enterprise

قواعد مطابقة النطاق: متى يُقبل النطاق الفرعي؟

تحقّق reCAPTCHA من النطاق ليس مطابقة صارمة افتراضياً؛ فالسلوك يعتمد على إعدادات مالك الموقع، والجدول التالي يلخّص الأصول المقبولة حسب النطاق المسجّل:

النطاق المسجّل الأصول المقبولة
example.com example.com وwww.example.com وsub.example.com (إذا فُعّل حرف البدل)
www.example.com www.example.com فقط (في الوضع الصارم)
*.example.com أي نطاق فرعي من example.com
localhost localhost فقط (للتطوير المحلي)

كيف يتصرف الخادم مع اسم المضيف

يعكس حقل hostname الصفحة التي تولّد عليها الرمز، ويبقى القرار للخادم: قبول متساهل يسمح بأي نطاق فرعي، أو صارم يشترط تطابقاً حرفياً.

# Permissive validation (accepts any subdomain)
def validate_permissive(token, secret, base_domain):
    result = requests.post(
        "https://www.google.com/recaptcha/api/siteverify",
        data={"secret": secret, "response": token},
    ).json()

    if not result.get("success"):
        return False

    hostname = result.get("hostname", "")
    return hostname == base_domain or hostname.endswith(f".{base_domain}")


# Strict validation (exact match only)
def validate_strict(token, secret, expected_hostname):
    result = requests.post(
        "https://www.google.com/recaptcha/api/siteverify",
        data={"secret": secret, "response": token},
    ).json()

    return result.get("success") and result.get("hostname") == expected_hostname

مثال عملي: نطاق يعيد التوجيه في متجر عربي

تخيّل متجراً سعودياً نطاقه الجذر matjar.sa، لكنه يعيد الزوار إلى www.matjar.sa، وصفحة الدخول تنتقل إلى نطاق فرعي للمصادقة مثل account.matjar.sa. لو مرّرت pageurl بقيمة https://matjar.sa/login بينما يُرسَل الرمز فعلياً من account.matjar.sa، فسيحمل الرمز اسم مضيف خاطئاً ويُرفض بصمت.

الحل خطوتان:

  1. اتبع سلسلة إعادة التوجيه (كما في الإصلاح الثالث أدناه) للوصول إلى النطاق النهائي.
  2. استخدم ذلك النطاق بالضبط في pageurl عند إرسال طلب الحل.

وهذا النمط شائع في منصّات المنطقة التي تفصل نطاق المصادقة عن الرئيسي.


إصلاح أخطاء النطاق في الأتمتة

معظم أخطاء النطاق تُحل بضبط pageurl بدقة، وفيما يلي أربع طرق من الأبسط إلى الأعمق:

الإصلاح الأول: اجعل pageurl مطابقاً تماماً للهدف

أكثر الإصلاحات شيوعاً هو أن يطابق pageurl النطاق الذي سيُرسَل إليه الرمز:

# Correct: pageurl matches where you'll submit the token
target_url = "https://www.example.com/login"

submit = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": "6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp",
    "pageurl": target_url,  # Must match the actual domain
    "json": 1,
})

الإصلاح الثاني: التعامل مع www وغير www

إن لم تعرف الصيغة التي يعتمدها الموقع، اطلب الصفحة واترك المكتبة تتبع إعادة التوجيه:

from urllib.parse import urlparse

def normalize_url(url):
    """Normalize URL for consistent domain matching."""
    parsed = urlparse(url)
    # Use exactly what the target site uses
    # Check if the site redirects www → non-www or vice versa
    return f"{parsed.scheme}://{parsed.netloc}{parsed.path}"

# Test which variant the site uses
response = requests.get("https://example.com/login", allow_redirects=True)
actual_url = response.url  # May be https://www.example.com/login after redirect

الإصلاح الثالث: اكتشاف النطاق الصحيح من سلسلة إعادة التوجيه

بعض المواقع تعيد التوجيه عبر أكثر من نطاق قبل الصفحة النهائية:

def get_final_url(url):
    """Follow redirects to find the actual CAPTCHA page domain."""
    response = requests.get(url, allow_redirects=True, timeout=15)
    return response.url

# Login URL might redirect:
# https://example.com/login → https://auth.example.com/login
final_url = get_final_url("https://example.com/login")
# Use final_url as pageurl for solver

الإصلاح الرابع: استخراج النطاق من إطار reCAPTCHA

حين يصعب تحديد النطاق من العنوان وحده، اقرأ إطار (iframe) الخاص بـ reCAPTCHA لاستخراج النطاق الذي يُربط به الرمز:

from bs4 import BeautifulSoup
from urllib.parse import urlparse

def extract_recaptcha_domain(html, page_url):
    """Extract the domain reCAPTCHA uses for token binding."""
    soup = BeautifulSoup(html, "html.parser")

    # Check for reCAPTCHA iframe
    iframe = soup.find("iframe", src=lambda s: s and "recaptcha" in s)
    if iframe:
        src = iframe.get("src", "")
        # The iframe URL may contain the domain parameter
        if "domain=" in src:
            # Extract domain from iframe URL
            pass

    # Default: use the page URL's domain
    return urlparse(page_url).netloc

أداة تشخيص تطابق النطاق

تجمع الأداة التالية الفحوصات السابقة في مكان واحد:

  • تتبع سلسلة إعادة التوجيه حتى النطاق النهائي.
  • تقارن صيغتي www وغير www.
  • تقترح قيمة pageurl الصحيحة الجاهزة للاستخدام.
import requests
from urllib.parse import urlparse

class DomainDiagnostic:
    """Diagnose domain verification issues for reCAPTCHA solving."""

    def __init__(self, target_url):
        self.target_url = target_url
        self.issues = []

    def check_redirects(self):
        """Check if the URL redirects to a different domain."""
        try:
            response = requests.get(
                self.target_url, allow_redirects=True, timeout=15,
                headers={"User-Agent": "Mozilla/5.0 Chrome/120.0.0.0"},
            )
            final_url = response.url
            original_domain = urlparse(self.target_url).netloc
            final_domain = urlparse(final_url).netloc

            if original_domain != final_domain:
                self.issues.append({
                    "type": "redirect",
                    "message": f"Redirects from {original_domain} to {final_domain}",
                    "fix": f"Use pageurl: {final_url}",
                })

            return final_url
        except Exception as e:
            self.issues.append({"type": "error", "message": str(e)})
            return self.target_url

    def check_www_variant(self):
        """Check if www and non-www point to the same content."""
        parsed = urlparse(self.target_url)
        domain = parsed.netloc

        if domain.startswith("www."):
            alt_domain = domain[4:]
        else:
            alt_domain = f"www.{domain}"

        alt_url = self.target_url.replace(domain, alt_domain)

        try:
            alt_response = requests.get(alt_url, allow_redirects=True, timeout=10)
            alt_final = urlparse(alt_response.url).netloc

            if alt_final != domain and alt_final != alt_domain:
                self.issues.append({
                    "type": "www_redirect",
                    "message": f"{alt_domain} redirects to {alt_final}",
                })
        except Exception:
            pass

    def report(self):
        """Generate diagnostic report."""
        final_url = self.check_redirects()
        self.check_www_variant()

        print(f"Target URL: {self.target_url}")
        print(f"Final URL:  {final_url}")
        print(f"Use as pageurl: {final_url}")

        if self.issues:
            print("\nIssues found:")
            for issue in self.issues:
                print(f"  [{issue['type']}] {issue['message']}")
                if "fix" in issue:
                    print(f"  Fix: {issue['fix']}")
        else:
            print("\nNo domain issues detected.")


# Usage
diag = DomainDiagnostic("https://example.com/login")
diag.report()

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

لماذا يُقبل الرمز في المتصفح ويُرفض عند إرساله من السكربت؟

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

كيف أعرف النطاق الصحيح الذي أضعه في pageurl؟

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

هل يؤثر اختلاف النطاق الفرعي على قبول الرمز؟

قد يؤثر، والسلوك يعتمد على إعداد مالك الموقع:

  • مع حرف البدل (*.example.com) تُقبل النطاقات الفرعية.
  • مع التحقق الصارم قد يُرفض رمز sub.example.com عند إرساله إلى example.com.

لذلك عامل كل نطاق فرعي كنطاق مستقل عند ضبط pageurl.

هل يضبط CaptchaAI تطابق النطاق نيابةً عني؟

لا. يولّد CaptchaAI رمزاً مربوطاً بقيمة pageurl التي تمرّرها أنت فقط، دون التحقق من إعدادات مالك الموقع. مسؤوليتك أن يطابق هذا العنوان النطاق الذي سيُرسَل إليه الرمز.

ما الفرق بين خطأ invalid-input-response وعدم تطابق اسم المضيف؟

الفرق في مصدر الرفض:

  • invalid-input-response: الرمز نفسه غير مقبول (منتهٍ أو مستهلَك أو من نطاق مختلف) — رفض من Google.
  • عدم تطابق اسم المضيف: يظهر مع success: true لكن حقل hostname يشير إلى نطاق يرفضه منطق خادمك أنت.

الخلاصة

يربط reCAPTCHA كل رمز باسم المضيف الذي تولّد عليه، ويبقى عدم تطابق pageurl أكثر أسباب الفشل شيوعاً في الأتمتة: العنوان الذي تمرّره إلى CaptchaAI يجب أن يطابق النطاق الذي سيُرسَل إليه الرمز. ثلاث خطوات تحوّل أخطاء النطاق الصامتة إلى مشكلة قابلة للتتبع:

  • اتبع سلسلة إعادة التوجيه للوصول إلى النطاق الفعلي.
  • تعامل مع صيغتي www وغير www بحسب ما يعتمده الموقع.
  • استعن بأداة التشخيص لكشف عدم التطابق قبل الحل.

مقالات ذات صلة

أدلة ذات صلة

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