حل Cloudflare Turnstile من Node.js يتلخّص في ثلاث خطوات: استخرج قيمة sitekey التي تبدأ بـ 0x من صفحة الهدف، أرسلها إلى CaptchaAI عبر method=turnstile، ثم أعد الرمز الناتج مع بيانات النموذج في الحقل cf-turnstile-response. لا حاجة إلى متصفح كامل ولا مكتبات إضافية؛ دالة fetch المدمجة في Node.js 18 تكفي.
الصعوبة ليست في الحل بل فيما يحيط به: sitekey يتغيّر بين الاختبار والإنتاج، ومعلمة action تظهر في بعض التطبيقات دون غيرها، ورمز صالح يُرفض لأنه أُرسل متأخراً. وهذه الحالات بالذات هي ما يعالجه ما يلي.
ما الذي يميّز Turnstile عن reCAPTCHA على مستوى الكود
الفارق الأول في شكل المفتاح: مفاتيح Turnstile تبدأ بـ 0x ومفاتيح reCAPTCHA بـ 6L، ومفتاح يبدأ بـ 6Le يعني method=userrecaptcha لا method=turnstile. وبقية الفروق تظهر أثناء التنفيذ:
- اسم الحقل: يُرسل الرمز باسم
cf-turnstile-responseداخل جسم الطلب. - الظهور: كثير من التطبيقات غير مرئية بالكامل، فلا مربّع اختيار تنتظر ظهوره.
- المعلمات: قد يمرّر الموقع
actionوcDataإلى الودجت، فترافق القيم نفسها طلب الحل. - السرعة: تحديات Turnstile تُحل عادةً في أقل من 10 ثوانٍ، فاجعل فواصل الاستطلاع الدوري قصيرة.
قبل أن تبدأ: ما تحتاجه فعلاً
- Node.js 18 أو أحدث — يوفّر
fetchأصلياً دون Axios أو مكتبة خارجية. - مفتاح الـ API من لوحة التحكم في CaptchaAI، محفوظاً في متغيّر بيئة لا مكتوباً داخل الكود.
- عنوان الصفحة التي تعرض الودجت، لا عنوان نقطة النهاية التي تستقبل النموذج.
والفوترة على عدد الـ threads المتزامنة لا على عدد عمليات الحل: خطة BASIC بـ 15 دولاراً شهرياً تتيح 5 threads، وخطة ADVANCE بـ 90 دولاراً شهرياً تتيح 50 thread، وعمليات الحل داخل كل thread غير محدودة. عملياً: سكربت متسلسل يكفيه أصغر خطة، والتوسّع يعني رفع الخطة لا شراء حزم.
الخطوة 1: استخرج sitekey من صفحة Turnstile
الدالة التالية تجلب HTML الصفحة وتجرّب أربعة أنماط بالترتيب: data-sitekey داخل عنصر cf-turnstile، ثم أي عنصر يحمل مفتاحاً يبدأ بـ 0x، ثم استدعاء turnstile.render، وأخيراً أي sitekey داخل سكربت مضمّن.
async function extractTurnstileSitekey(url) {
const resp = await fetch(url, {
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
},
});
const html = await resp.text();
// Method 1: data-sitekey attribute on Turnstile div
const divMatch = html.match(
/class=["'][^"]*cf-turnstile[^"]*["'][^>]*data-sitekey=["']([0-9x][A-Za-z0-9_-]+)["']/
);
if (divMatch) return divMatch[1];
// Method 2: data-sitekey on any element (Turnstile keys start with 0x)
const attrMatch = html.match(
/data-sitekey=["'](0x[A-Za-z0-9_-]+)["']/
);
if (attrMatch) return attrMatch[1];
// Method 3: In JavaScript turnstile.render call
const jsMatch = html.match(
/turnstile\.render\s*\([^,]+,\s*\{[^}]*sitekey\s*:\s*["']([0-9x][A-Za-z0-9_-]+)["']/
);
if (jsMatch) return jsMatch[1];
// Method 4: Generic sitekey in inline script
const inlineMatch = html.match(
/sitekey\s*:\s*["'](0x[A-Za-z0-9_-]+)["']/
);
if (inlineMatch) return inlineMatch[1];
return null;
}
إذا عادت الدالة بـ null فالودجت غالباً يُحقن بعد اكتمال التحميل. اجلب الصفحة عندئذٍ عبر Puppeteer أو Playwright واقرأ السمة بعد العرض، ثم أكمل التدفق دون تغيير.
الخطوة 2: أرسل المهمة إلى CaptchaAI واستطلع النتيجة
الإرسال طلب POST واحد إلى in.php يحمل method=turnstile وsitekey وpageurl، وتعيد الاستجابة معرّف المهمة. بعدها تستفسر عن النتيجة من res.php حتى تصل الحالة إلى 1.
const API_KEY = "YOUR_API_KEY";
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function solveTurnstile(sitekey, pageurl, action = null) {
// Submit task
const submitData = {
key: API_KEY,
method: "turnstile",
sitekey: sitekey,
pageurl: pageurl,
json: "1",
};
if (action) {
submitData.action = action;
}
const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
method: "POST",
body: new URLSearchParams(submitData),
});
const submitResult = await submitResp.json();
if (submitResult.status !== 1) {
throw new Error(`Submit error: ${submitResult.request}`);
}
const taskId = submitResult.request;
console.log(`Task ID: ${taskId}`);
// Poll for result
for (let i = 0; i < 30; i++) {
await sleep(5000);
const pollResp = await fetch(
`https://ocr.captchaai.com/res.php?${new URLSearchParams({
key: API_KEY,
action: "get",
id: taskId,
json: "1",
})}`
);
const pollResult = await pollResp.json();
if (pollResult.status === 1) {
return pollResult.request;
}
if (pollResult.request === "ERROR_CAPTCHA_UNSOLVABLE") {
throw new Error("Turnstile unsolvable");
}
}
throw new Error("Solve timed out");
}
ثلاث ملاحظات تشغيلية:
- الفاصل بين محاولات الاستفسار خمس ثوانٍ، وهو مناسب لنوع يُحل في أقل من 10 ثوانٍ.
ERROR_CAPTCHA_UNSOLVABLEيعني انتهاء المهمة دون نتيجة؛ أعد الاستخراج والإرسال بدل مواصلة الاستفسار.- احتفظ بمعرّف المهمة في السجل؛ هو أسرع طريق لتتبّع طلب بعينه في لوحة التحكم.
الخطوة 3: مرّر الرمز في حقل cf-turnstile-response
الرمز الناتج ليس نهاية العملية بل حقل إضافي في النموذج: أضفه إلى بيانات الطلب باسم cf-turnstile-response، وأرسله من الجلسة نفسها التي جلبت الصفحة.
async function submitTurnstileForm(url, formData, token) {
const body = new URLSearchParams({
...formData,
"cf-turnstile-response": token,
});
const resp = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
},
body,
});
return {
status: resp.status,
body: await resp.text(),
};
}
أرسل الرمز فور استلامه: رموز Turnstile قصيرة العمر، وكل تأخير بين الحل والإرسال يرفع احتمال الرفض حتى مع رمز صحيح.
تدفق تسجيل دخول كامل من الطرف إلى الطرف
تجميع الخطوات الثلاث في دالة واحدة يوضّح الترتيب: استخراج، ثم حل، ثم إرسال.
async function loginWithTurnstile(loginUrl, credentials) {
// Step 1: Extract sitekey
const sitekey = await extractTurnstileSitekey(loginUrl);
if (!sitekey) {
throw new Error("Turnstile sitekey not found");
}
console.log(`Sitekey: ${sitekey}`);
// Step 2: Solve Turnstile
const token = await solveTurnstile(sitekey, loginUrl);
console.log(`Token: ${token.substring(0, 50)}...`);
// Step 3: Submit form
const result = await submitTurnstileForm(loginUrl, credentials, token);
console.log(`Result: ${result.status}`);
return result;
}
// Usage
const result = await loginWithTurnstile("https://example.com/login", {
email: "[email protected]",
password: "pass123",
});
فئة جاهزة للإنتاج تجمع الاكتشاف والحل
في الإنتاج غلّف المنطق في فئة واحدة تحمل مفتاح الـ API في حقل خاص وتتيح إعادة الاستخدام عبر عدة صفحات.
class TurnstileSolver {
#apiKey;
constructor(apiKey) {
this.#apiKey = apiKey;
}
async solve(sitekey, pageurl, options = {}) {
const taskId = await this.#submit(sitekey, pageurl, options);
return await this.#poll(taskId);
}
async detectAndSolve(url) {
const sitekey = await this.#detect(url);
if (!sitekey) throw new Error("No Turnstile found");
return await this.solve(sitekey, url);
}
async #detect(url) {
const resp = await fetch(url, {
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0" },
});
const html = await resp.text();
const match = html.match(/data-sitekey=["'](0x[A-Za-z0-9_-]+)["']/);
return match ? match[1] : null;
}
async #submit(sitekey, pageurl, options) {
const body = new URLSearchParams({
key: this.#apiKey,
method: "turnstile",
sitekey,
pageurl,
json: "1",
...(options.action && { action: options.action }),
...(options.cdata && { data: options.cdata }),
});
const resp = await fetch("https://ocr.captchaai.com/in.php", {
method: "POST",
body,
});
const data = await resp.json();
if (data.status !== 1) throw new Error(`Submit: ${data.request}`);
return data.request;
}
async #poll(taskId) {
const params = new URLSearchParams({
key: this.#apiKey,
action: "get",
id: taskId,
json: "1",
});
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 5000));
const resp = await fetch(`https://ocr.captchaai.com/res.php?${params}`);
const data = await resp.json();
if (data.status === 1) return data.request;
if (data.request === "ERROR_CAPTCHA_UNSOLVABLE") {
throw new Error("Unsolvable");
}
}
throw new Error("Timed out");
}
}
// Usage
const solver = new TurnstileSolver("YOUR_API_KEY");
const token = await solver.detectAndSolve("https://example.com/login");
تسهّل هذه البنية إضافة ما تحتاجه فرق الإنتاج: إعادة المحاولة بالتراجع الأسي، وحد أقصى للتزامن يطابق عدد الـ threads، وتسجيل زمن كل عملية حل.
التعامل مع action وcData في تطبيقات Turnstile
تمرّر بعض المواقع معلمتي action وcData إلى الودجت، فيجب أن يحمل طلب الحل القيمتين نفسيهما وإلا رُفض الرمز عند التحقق.
// Extract action from the page
function extractTurnstileAction(html) {
const match = html.match(
/data-action=["']([^"']+)["']|action\s*:\s*["']([^"']+)["']/
);
return match ? match[1] || match[2] : null;
}
// Solve with action
const token = await solver.solve(sitekey, pageurl, {
action: "login",
cdata: "session_abc123",
});
ابحث عن data-action في HTML أو action: داخل استدعاء turnstile.render. غياب المعلمة يعني أن الموقع لا يستخدمها، فلا ترسل قيمة من عندك.
التحقق من الرمز على جانب الخادم
إذا كنت في الطرف الآخر وتبني خدمة تستقبل رموز Turnstile، فالتحقق يجري عبر نقطة نهاية siteverify من Cloudflare بالمفتاح السري للموقع.
async function verifyTurnstileToken(token, ip) {
const resp = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
secret: "YOUR_TURNSTILE_SECRET_KEY",
response: token,
remoteip: ip,
}),
}
);
const data = await resp.json();
return data.success;
}
وهذا مفيد لفرق اختبار الجودة التي تحتاج بيئة كاملة الطرفين: خادم يتحقق، وسكربت يحلّ، ودورة اختبار آلية تعمل ليلاً.
سيناريو تطبيقي: مراقبة بوابة حجز مواعيد
فريق في القاهرة يراقب المواعيد المتاحة على بوابة حجز محمية بـ Turnstile، ويفحص التوافر كل بضع دقائق من خادم واحد. أربعة قرارات تفصل بين تدفق مستقر وآخر هشّ:
- استخرج sitekey مرة واحدة عند بدء التشغيل واحتفظ به في الذاكرة، وأعد الاستخراج عند أول فشل في التحقق.
- اربط عدد العمال المتوازين بعدد الـ threads في خطتك: خمسة عمال على خطة تتيح خمسة threads، لا أكثر.
- سجّل زمن كل دورة من الإرسال إلى استجابة البوابة؛ ارتفاعه غالباً مؤشر على تغيّر في الصفحة لا على مشكلة في الخدمة.
- شغّل الفحص على فترات ثابتة بدل رشقات متلاحقة؛ الأتمتة المنتظمة أخف أثراً وأقل عرضة لتحديد معدل الطلبات.
والمبدأ نفسه ينطبق على أي تدفق متكرّر: متابعة أسعار، أو اختبار انحدار ليلي، أو مراقبة مخزون.
جدول الأعطال الشائعة
معظم حالات الفشل تعود إلى عدد محدود من الأسباب المتكرّرة:
| ما تراه | السبب المرجّح | الإصلاح |
|---|---|---|
مفتاح الموقع يبدأ بـ 6Le |
العنصر reCAPTCHA وليس Turnstile | بدّل إلى method=userrecaptcha |
| الرمز مرفوض عند التحقق | مفتاح موقع خاطئ أو رمز أُرسل متأخراً | أعد استخراج المفتاح وأرسل الرمز فوراً |
| لم يُعثر على sitekey | الودجت يُحقن عبر JavaScript بعد التحميل | اقرأ الصفحة عبر Puppeteer أو Playwright |
ERROR_BAD_PARAMETERS |
sitekey أو pageurl مفقود من الطلب |
تأكد من إرسال الحقلين معاً |
| استجابة 403 بعد الإرسال | رؤوس الطلب لا تشبه متصفحاً حقيقياً | استخدم User-Agent واقعياً وحافظ على الجلسة |
| الرمز صالح لكن الطلب يفشل | action أو cData غير مطابق للصفحة |
مرّر القيم كما وردت في الودجت |
الأسئلة المتداولة
هل أحتاج إلى متصفح مثل Puppeteer لحل Turnstile؟
لا في معظم الحالات. إذا كان الودجت في HTML الأولي فطلب fetch واحد يكفي. المتصفح ضروري فقط حين يُحقن الودجت بعد التحميل، أو حين تسبق النموذجَ خطوات تفاعلية.
كم يبقى رمز Turnstile صالحاً قبل أن يُرفض؟
المدة قصيرة ويحدّدها Cloudflare لا خدمة الحل. القاعدة العملية: أرسل الرمز في الطلب التالي مباشرة، ولا تخزّنه لجلسة أخرى.
لماذا أحصل على استجابة 403 رغم أن الرمز صحيح؟
الرمز ليس العامل الوحيد. تحقّق من أن الطلب يستخدم الجلسة نفسها التي جُلبت منها الصفحة، وأن pageurl يطابق العنوان حرفياً، وأن الرؤوس تشبه متصفحاً حقيقياً. للتفاصيل راجع إصلاح خطأ 403 بعد إرسال رمز Turnstile.
كم عدد الـ threads الذي أحتاجه لتشغيل عدة سكربتات معاً؟
عدد الـ threads سقف المهام المتزامنة لا سقف عمليات الحل. سكربت متسلسل يشغل thread واحداً، وخمسة عمال متوازين يحتاجون خمسة threads. القرار مرتبط بدرجة التزامن وحدها.
هل يعمل التدفق نفسه مع Cloudflare Challenge؟
لا. Cloudflare Challenge نوع منفصل له method خاص به في CaptchaAI ولا يُحل بإرسال method=turnstile. ميّز بينهما قبل كتابة الكود عبر الفرق بين Cloudflare Challenge وTurnstile.
الخلاصة
التدفق كله ثلاث خطوات: استخرج مفتاح موقع يبدأ بـ 0x، أرسله إلى CaptchaAI عبر method=turnstile، ثم مرّر الرمز في cf-turnstile-response من الجلسة نفسها ودون تأخير. وما تبقّى تفاصيل تُبنى فوق هذا الأساس.
مقالات ذات صلة
- كيف تميّز بين Cloudflare Challenge وTurnstile
- GeeTest أم Cloudflare Turnstile: أيهما تواجه ومتى
- إصلاح خطأ 403 بعد إرسال رمز Turnstile