من المحتمل أن يخفق الطلب 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
.
ويسبب الدخول إلى أي ترويسات أخرى خطأً.
اقتباسلاحظ أنه لا وجود للترويسة
Content-Length
في القائمة السابقة، والتي تحوي الحجم الكامل للاستجابة، فإذا كنا بصدد تنزيل شيء ما وأردنا تتبع النسبة المئوية لتقدم العملية، فلا بدّ من الحصول على سماحية أخرى للوصول إلى الترويسة.
لمنح إمكانية الوصول إلى ترويسة الاستجابة، ينبغي أن يُرسل الخادم الترويسة 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، وكل ما ستحصل عليه هو الاستجابة إلى الطلب الرئيسي، أو الخطأ إذا لم يأذن الخادم بإرسال الطلب.
يمكن لشيفرة 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
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.