البحث في الموقع
المحتوى عن 'cross-origin'.
-
من المحتمل أن يخفق الطلب fetch المرسَل إلى موقع ويب آخر، مثلًا: ستخفق محاولة الحصول على http://example.com: try { await fetch('http://example.com'); } catch(err) { alert(err); // Failed to fetch } لا بدّ من الإشارة أولًا إلى المفاهيم البنيوية لموضوعنا: الأصل origin: وهو الثلاثية (نطاق ومنفذ وبروتوكول). الطلبات ذات الأصل المختلط cross-origin requests: وهي الطلبات المرسَلة إلى نطاق (أو نطاق فرعي) آخر أو عبر منفذ آخر أو باستخدام بروتوكول آخر، وتتطلب ترويسات خاصةً من الجانب البعيد. تدعى هذه السياسة "CROS" وهو اختصار للعبارة "Cross-Origin Resource Sharing"، وتعني مشاركة الموارد ذات الأصول المختلطة. لماذا نحتاج إلى CROS؟ لمحة تاريخية موجزة وُجِدت هذه السياسة لحماية الإنترنت من المخترقين، فلسنوات عديدة لم يكن مسموحًا لسكربت من موقع ما أن يصل إلى محتوى موقع آخر، حيث لا يمكن لسكربت مشبوه من الموقع hacker.com مثلًا الوصول إلى صندوق البريد الإلكتروني لمستخدم على الموقع gmail.com، مما أشعَر مستخدمي الإنترنت في ذلك الوقت بالأمان لعدم امتلاك جافاسكريبت JavaScript، أي توابع خاصة لتنفيذ طلبات عبر الشبكة، فقد كانت عبارةً عن لغة للتسلية وتزيين صفحات الويب، إلا أن مطوري الويب احتاجوا سريعًا إلى قوة أكبر للتحكم بالصفحات، فاخترعوا أساليب متنوعةً للالتفاف على محدودية اللغة وإرسال الطلبات إلى مواقع أخرى. استخدام النماذج لقد كانت إحدى طرق التواصل مع خادم آخر هي إرسال نموذج <form> إليه، وذلك باستخدام الإطارات <iframe> لإبقاء الزوار ضمن الصفحة نفسها: <!-- form target --> <iframe name="iframe"></iframe> <!-- a form could be dynamically generated and submited by JavaScript --> <form target="iframe" method="POST" action="http://another.com/…"> ... </form> وهكذا تمكن الناس من إرسال طلبات GET/POST إلى مواقع أخرى دون وجود توابع لتنفيذ ذلك، لأنّ النماذج قادرة على إرسال البيانات إلى أي مكان، لكن لم يكن بالإمكان الحصول على الاستجابة لأنّ الوصول إلى محتويات الإطار <iframe> غير مسموح، ولنكون دقيقين؛ وُجِدت بعض الحيل للالتفاف على ذلك أيضًا، لكنها تطلبت سكربتًا خاصًا يوضع ضمن الإطار والصفحة، أي صار التواصل بينهما ممكنًا من الناحية التقنية. استخدام السكربتات اعتمدت إحدى الحيل المستخدَمة أيضًا على المعرّف <script>، إذ يمكن أن تكون قيمة الخاصية src لسكربت هي اسم أي نطاق أو موقع مثل: <script src="http://another.com/…"> وبالتالي يمكن تنفيذ سكربت أيًا كان مصدره، فإذا أراد موقع ما مثل another.com إتاحة أمكانية الوصول لبياناته، فسيستخدم البروتوكول "JSON with padding" واختصاره JSNOP، وإليك آلية عمله: لنفترض أننا نريد الوصول إلى بيانات الطقس على الموقع http://another.com إنطلاقًا من موقعنا: نعرّف في البداية دالةً عامةً لاستقبال البيانات ولتكن gotWeather. // صرح عن الدالة التي ستعالج البيانات المطلوبة function gotWeather({ temperature, humidity }) { alert(`temperature: ${temperature}, humidity: ${humidity}`); } ننشئ <script>، وتكون قيمة الخاصية src فيه هي src="http://another.com/weather.json?callback=gotWeather" وللمستخدمين اسم الدالة العامة كقيمة للمعامل callback الخاص بالعنوان URL. let script = document.createElement('script'); script.src = `http://another.com/weather.json?callback=gotWeather`; document.body.append(script); يوّلد الخادم البعيد another.com ديناميكيًا سكربتًا يستدعي الدالة ()gotWeatherبالبيانات التي يريدنا أن نحصل عليها. // ستبدو الاستجابة التي نتوقعها من الخادم كالتالي gotWeather({ temperature: 25, humidity: 78 }); عندما يُحمَّل السكربت الذي يولده الخادم ويُنفَّذ، ستُنفَّذ الدالة gotWeather ونحصل على البيانات. سيعمل الأسلوب السابق ولن يشكل خرقًا لأمن الموقع البعيد بسبب اتفاق كلا الطرفين على تبادل المعلومات بهذا الشكل، ولهذا لن تُعَدَّ العملية عندها اختراقًا، ولا زالت بعض الخدمات تتبع نفس الأسلوب في الوصول إلى البيانات البعيدة وتعمل حتى على المتصفحات القديمة جدًا. ظهرت بعد فترة من الزمن ضمن لغة JavaScript توابع الطلبات عبر الشبكة والتي ينفذها المتصفح، وقد رفضت الطلبات ذات الأصول المختلطة في البداية، إلا أنه سُمح باستخدامها نتيجة نقاشات طويلة، بشرط الحصول على سماحيات صريحة من الخادم لتنفيذ أي متطلبات، ويُعبَّر عنها من خلال ترويسات خاصة. الطلبات الآمنة هنالك نوعان من الطلبات ذات الأصل المختلط: الطلبات الآمنة safe requests. بقية الأنواع. من السهل إنشاء الطلبات الآمنة لذلك سنبدأ بها، إذ يُعَد الطلب آمنًا إذا حقق الشرطين التاليين: يستخدم نوعًا آمنًا مثل GET أو POST أو HEAD. يستخدم ترويسات آمنةً. ويسمح بالترويسات المخصصة التالية فقط: Accept. Accept-Language. Content-Language. Content-Type: بحيث تحمل إحدى القيم التالية application/x-www-form-urlencoded أو multipart/form-data أو text/plain. وتُعَد بقية الطلبات "غير آمنة"، حيث لا تطابق الطلبات باستخدام PUT أو باستخدام الترويسة API-Key معايير الأمان السابقة مثلًا. ويكمن الفرق الجوهري في إمكانية تنفيذ الطلبات الآمنة باستخدام معرِّف النموذج <form> أو معرّف السكربت <script> دون الحاجة لأي توابع خاصة، وبالتالي ستكون أقدم الخوادم قادرةً على استقبالها. لا يمكن في المقابل استخدام الطلبات التي لا تمتلك ترويسات معياريةً مثل DELETE بهذه الطريقة، ولفترة طويلة لم تكن JavaScript قادرةً على استخدام هذا النوع من الطلبات، وهكذا سيفترض الخادم القديم أن هذه الطلبات قادمة من مصدر مخوّل بذلك، لأنه يتوقع أنّ صفحة الويب غير قادرة على إرسال هذه الطلبات. عندما نرسل طلبًا غير آمن، فسيرسل المتصفح طلبًا تمهيديَا preflight، سيسأل الخادم فيه إن كان سيوافق على طلبات ذات أصول مختلطةً أم لا، فإن لم يؤكد الخادم ذلك صراحةً من خلال الترويسات، فلن يُرسَل الطلب غير الآمن. سياسة CROS للطلبات غير الآمنة سيضيف المتصفح دائمًا الترويسة Origin إلى الطلب من الأصول المختلطة، فإذا طلبنا المورد https://anywhere.com/request من الموقع https://javascript.info/page مثلًا؛ فستبدو الترويسات بالشكل التالي: GET /request Host: anywhere.com Origin: https://javascript.info ... تحتوي الترويسة origin كما نرى الأصل (نطاق وبروتوكول ومنفذ) كما هو لكن دون مسار، ويمكن للخادم أن يتحقق من الترويسة Origin، فإن وافق على قبول هذا الطلب، فسيضيف ترويسةً خاصةً هي Access-Control-Allow-Origin إلى الاستجابة، وينبغي أن تحتوي الترويسة على الأصل المقبول (https://javascript.info في حالتنا) أو رمز النجمة (*)، عندها سيكون الطلب ناجحًا وإلا فسيُعَد خاطئًا. يلعب المتصفح دور الوسيط الموثوق حيث: يضمن إرسال الأصل الصحيح في الطلب ذي الأصول المختلطة. يتحقق من وجود السماحية Access-Control-Allow-Origin في الاستجابة، فإذا وُجدَت فسيسمح لشيفرة JavaScript بالوصول إلى الاستجابة وإلا ستخفق العملية وسيحدث خطأ. 200 OK Content-Type:text/html; charset=UTF-8 Access-Control-Allow-Origin: https://javascript.info ترويسات الاستجابة افتراضيًا قد لا تتمكن JavaScript من الوصول إلا إلى الترويسات الآمنة للاستجابة عند إرسال طلبات ذات أصل مختلط، وهذه الترويسات هي: Cache-Control. Content-Language. Content-Type. Expires. Last-Modified. Pragma. ويسبب الدخول إلى أي ترويسات أخرى خطأً. لمنح إمكانية الوصول إلى ترويسة الاستجابة، ينبغي أن يُرسل الخادم الترويسة Access-Control-Expose-Headers، والتي تتضمن قائمةً بأسماء الترويسات غير الآمنة التي يُفترض جعلها قابلةً للوصول، وتفصل بينها فاصلة، كالمثال التالي: 200 OK Content-Type:text/html; charset=UTF-8 Content-Length: 12345 API-Key: 2c9de507f2c54aa1 Access-Control-Allow-Origin: https://javascript.info Access-Control-Expose-Headers: Content-Length,API-Key وبوجود ترويسة مثل Access-Control-Expose-Headers سيُسمح للسكربت بقراءة ترويستي الاستجابة Content-Length وAPI-Key. الطلبات غير الآمنة يمكن استخدام جميع طلبات HTTP مثل PATCH وDELETE وغيرها، وليس فقط GET/POST، ولم يتخيل أحد في السابق إمكانية تنفيذ صفحات الويب لهذه الطلبات، لذلك قد تجد بعض خدمات الويب التي تعامل الطلبات غير المعيارية مثل إشارة "بأنها طلبات مصدرها ليس المتصفح"، ويمكنها أن تأخذ هذا الأمر في الحسبان عندما تتحقق من حقوق الوصول، ولتفادي سوء الفهم، لن ينفِّذ المتصفح أي طلبات غير آمنة كانت قابلة للتنفيذ مباشرةً فيما مضى، وسيرسل طلبًا تمهيديًا preflight إلى الخادم لطلب الإذن، ويستخدم الطلب التمهيدي التابع OPTIONS دون جسم للطلب، وبوجود ترويستين: الترويسة Access-Control-Request-Method: وتؤمن تابعًا للطلب غير الآمن. الترويسة Access-Control-Request-Headers: وتؤمن قائمةً بترويسات غير آمنة تفصل بينها فاصلة. إذا وافق الخادم على تنفيذ الطلبات، فسيرسل استجابةً بجسم فارغ ورمز الحالة 200، بالإضافة إلى الترويسات التالية: Access-Control-Allow-Origin: ويجب أن تحمل القيمة (*) أو أصل الموقع الذي أرسل الطلب، مثل "https://javascript.info"، ليُسمح له بالوصول. Access-Control-Allow-Methods: ويجب أن تحتوي التوابع المسموحة. Access-Control-Allow-Headers: ويحب أن تضم قائمةً بالترويسات المسموحة. Access-Control-Max-Age: وهي ترويسة إضافية يمكنها تحديد الفترة الزمنية (ثوانٍ) للاحتفاظ بالإذن، لذا لن يكون على المتصفح إرسال طلبات تمهيدية للطلبات اللاحقة التي تحقق السماحيات الممنوحة سابقًا. لنلق نظرةً على آلية العمل خطوةً خطوةً، بمثال عن طلب PATCH ذي أصول مختلطة (والذي يُستخدَم غالبًا لتحديث البيانات): let response = await fetch('https://site.com/service.json', { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'API-Key': 'secret' } }); لدينا ثلاثة أسباب لعدّ هذا الطلب غير آمن (ويكفي أحدها بالطبع): الطلب هو PATCH. لاتحمل الترويسة Content-Type إحدى القيم: application/x-www-form-urlencoded أو multipart/form-data أو text/plain. وجود الترويسة API-Key غير الآمنة. الخطوة 1: الطلب التمهيدي preflight يرسل المتصفح بنفسه -قبل إرسال طلب غير آمنٍ كهذا- طلبًا تمهيديًا له الشكل التالي: OPTIONS /service.json Host: site.com Origin: https://javascript.info Access-Control-Request-Method: PATCH Access-Control-Request-Headers: Content-Type,API-Key OPTIONS: تابع الطلب التمهيدي. /service.json: المسار، ويطابق مسار الطلب الرئيسي تمامًا. الترويسات الخاصة بالطلب ذي الأصل المختلط: Origin: أصل مُرسل الطلب. Access-Control-Request-Method: نوع الطلب Access-Control-Request-Headers:قائمة بترويسات غير آمنة تفصل بينها فاصلة. الخطوة 2: الاستجابة للطلب التمهيدي ينبغي أن يستجيب الخادم برمز الحالة 200 والترويسات التالية: Access-Control-Allow-Origin: https://javascript.info Access-Control-Allow-Methods: PATCH Access-Control-Allow-Headers: Content-Type,API-Key. تسمح هذه الاستجابة بالتواصل المستقبلي مع الخادم وإلا فسيقع خطأ. إذا كنت سترسل طلبات أو ترويسات من أنواع أخرى مستقبلًا، فمن المنطقي أن تطلب الإذن مسبقًا، وذلك بإضافتهم إلى القائمة أثناء الطلب التمهيدي، يوضح المثال التالي استجابةً يُسمح فيها باستخدام PUT وDELETE بالإضافة إلى ترويسات أخرى: 200 OK Access-Control-Allow-Origin: https://javascript.info Access-Control-Allow-Methods: PUT,PATCH,DELETE Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control Access-Control-Max-Age: 86400 سيرى المتصفح الآن الطلب PATCH في قائمة الطلبات المسموحة Access-Control-Allow-Methods، كما سيرى الترويسة Content-Type,API-Key في قائمة الترويسات المسموحة، لذا لن يتردد بإرسال الطلب الرئيسي المبني عليهما. إذا رأى المتصفح الترويسة Access-Control-Max-Age وقد أسندت إليها قيمة بالثواني، فسيحتفظ بالسماحيات التي مُنحت للطلب التمهيدي خلال هذه المدة الزمنية، أي سيُحتفظ بالسماحيات في مثالنا السابق فترة 86400 ثانية (أي يوم كامل)، ولن تحتاج الطلبات اللاحقة إلى نفس الخادم طلبات تمهيديةً أخرى، بفرض أنها تتلاءم مع السماحيات الممنوحة، وستُرسل مباشرةً. الخطوة 3: الطلب الفعلي يرسِل المتصفح الطلب الفعلي عندما ينجح الطلب التمهيدي، وتُنفَّذ العملية يطريقة مماثلة لإرسال طلب آمن. سيتضمن الطلب الرئيسي الترويسة Origin (لأنه طلب ذو أصل مختلط): PATCH /service.json Host: site.com Content-Type: application/json API-Key: secret Origin: https://javascript.info الخطوة 4: الاستجابة الفعلية لا بدّ للخادم من إضافة الترويسة Access-Control-Allow-Origin إلى الاستجابة الرئيسية، ولن يُعفيه الطلب التمهيدي الناجح من هذه المهمة: Access-Control-Allow-Origin: https://javascript.info تستطيع بعد ذلك قراءة استجابة الخادم الفعلية. يمكن لشيفرة JavaScript الآن قراءة الاستجابة على الطلب الفعلي. الثبوتيات Credentials لا تحضر الطلبات ذات الأصل المختلط التي تنتج عن شيفرة JavaScript أية ثبوتيات (ملفات تعريف الارتباط cookies أو استيثاق HTTP)، وهذا ليس أمرًا شائعًا في طلبات HTTP، فعند إرسال طلب HTTP إلى الموقع http://site.com مثلَا، فسيحمل الطلب جميع ملفات تعريف الارتباط الموجودة في نطاق المُرسِل، لكن الطلبات ذات الأصل المختلط الناتجة عن JavaScript تُمثّل استثناءً، حيث لن يُرسل الأمر (fetch(http://another.com أي ملفات تعريف ارتباط حتى تلك التي تنتمي إلى النطاق another.com. لكن لماذا؟ لأنّ الطلبات التي تُزوَّد بثبوتيات أقوى بكثير، إذ يمكن لشيفرة JavaScript -إن سُمح لها- أن تعمل بكامل إمكانياتها بالنيابة عن المستخدم، وأن تصل إلى معلومات حساسة بالاستفادة من هذه الثبوتيات. لكن هل يثق الخادم بسكربت ما إلى هذا الحد؟ إن كان الأمر كذلك، فلا بدّ من السماح صراحةً بالطلبات التي تحمل ثبوتيات من خلال ترويسة إضافية، حيث سنحتاج إلى إضافة الخيار credentials: "include" عند إرسال الثبوتيات مع fetchبالشكل التالي: fetch('http://another.com', { credentials: "include" }); يمكن الآن إرسال ملفات تعريف الارتباط التي تنتمي إلى another.com عبر الطلب fetch إلى الموقع الهدف، وينبغي على الخادم إذا وافق على قبول الثبوتيات، إضافة الترويسة Access-Control-Allow-Credentials: true إلى استجابته بالإضافة إلى الترويسة Access-Control-Allow-Origin، مثلًا: 200 OK Access-Control-Allow-Origin: https://javascript.info Access-Control-Allow-Credentials: true لاحظ أنه يُمنع استخدام النجمة (*) كقيمة للترويسة Access-Control-Allow-Origin في الطلبات التي تحمل ثبوتيات، إذ لا بدّ -كما نرى في المثال السابق- من تحديد الأصل بدقة، وهذا معيار إضافي للتأكد من أنّ الخادم يعرف تمامًا الجهة التي يثق بها لتنفيذ طلبات مثل هذه. خلاصة هنالك نوعان من الطلبات ذات الأصول المختلطة من وجهة نظر المتصفح: آمنة وغير آمنة. لا بدّ للطلبات الآمنة من تحقيق الشرطين التاليين: أن تستخدم نوعًا آمنًا مثل: GET أو POST أو HEAD. أن تستخدم الترويسات الآمنة التالية: Accept Accept-Language Content-Language Content-Type: وتحمل إحدى القيم application/x-www-form-urlencoded أو multipart/form-data أو text/plain. الفرق الجوهري هو أن الطلبات الآمنة ومنذ وقت طويل، تُنفَّذ باستخدام معرِّف النموذج <form> أو معرِّف السكربت <script>، بينما مُنعت المتصفحات من تنفيذ الطلبات غير الآمنة لفترة طويلة. يظهر هذا الفرق عمليًا في إمكانية إرسال الطلبات الآمنة مع الترويسة Origin مباشرةً، بينما يحتاج المتصفح إلى إرسال طلب تمهيدي preflight عند إرسال الطلبات غير الآمنة، يطلب فيها إذنًا من الخادم. للطلبات الآمنة: يرسل المتصفح الترويسة مع الأصل Origin. بالنسبة للطلبات التي لا تحمل ثبوتيات (لا ترسَل الثبوتيات بشكل افتراضي) لا بدّ أن يضبط الخادم ما يلي: Access-Control-Allow-Origin: على القيمة (*) أو نفس قيمة الأصل Origin. بالنسبة للطلبات التي تحمل ثبوتيات لا بدّ أن يضبط الخادم: Access-Control-Allow-Origin: على نفس قيمة الأصل Origin. Access-Control-Allow-Credentials: على القيمة "true". ولمنح JavaScript الوصول إلى ترويسات الاستجابة عدا Cache-Control و Content-Language وContent-Type وExpires وLast-Modified وPragma، فلا بدّ أن يضع الخادم الترويسة التي يُسمح بالوصول إليها ضمن الترويسة Access-Control-Expose-Headers. يُرسل المتصفح طلبًا تمهيديًا قبل الطلب الفعلي عند إرسال طلبات غير آمنة: يرسل المتصفح الطلب OPTIONS إلى نفس العنوان الذي سيرسِل إليه الطلب الفعلي مزوّدًا بالترويسات التالية: Access-Control-Request-Method: ويحمل نوع الطلب. Access-Control-Request-Headers: ويحمل قائمةً بترويسات غير آمنة يُطلب الإذن باستخدامها. يستجيب الخادم برمز الحالة 200، وبالترويسات التالية: Access-Control-Allow-Methods: تضم قائمةً بأنواع الطلبات المسموحة. Access-Control-Allow-Headers: تضم قائمةً بالترويسات المسموحة. Access-Control-Max-Age: وتحتوي على قيمة تمثل الفترة الزمنية (مقدرةً بالثواني) التي يُحتفظ فيها بالسماحيات. يُرسَل الطلب الفعلي بعد ذلك، وتُطبق خطوات إرسال الطلب الآمن. ترجمة -وبتصرف- للفصل Fetch: Cross-origin Requests من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: تتبع تقدم عملية التنزيل باستخدام Fetch وإلغاء العملية Fetch