بروتوكول Web Push

لقد رأينا كيف يمكن استخدام مكتبة لتفعيل الرسائل الفورية، ولكن ما هي وظيفة هذه المكتبات تحديدًا؟

حسنًا، إنّها تُرسل طلبات إلى الشبكة مع ضمان أن تكون هذه الطلبات بالتنسيق الصحيح. المواصفات التي تحدد طلب الشبكة هذا هي بروتوكول الإشعارات الفورية على الويب.

مخطّط بياني لإرسال رسالة دفع من الخادم إلى خدمة دفع

يوضّح هذا القسم كيف يمكن للخادم تعريف نفسه باستخدام مفاتيح خادم التطبيق وكيفية إرسال الحمولة المشفّرة والبيانات المرتبطة بها.

هذا ليس جانبًا جميلاً من الإشعارات الفورية على الويب، وأنا لست خبيرًا في التشفير، ولكن دعنا نلقي نظرة على كل جزء لأنّه من المفيد معرفة ما تفعله هذه المكتبات في الخلفية.

مفاتيح خادم التطبيقات

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

عندما نشغّل رسالة فورية، نرسل مجموعة من العناوين التي تتيح لخدمة الرسائل الفورية مصادقة التطبيق. (يتم تحديد ذلك بواسطة مواصفات VAPID.)

ماذا يعني كل هذا في الواقع وماذا يحدث بالضبط؟ في ما يلي الخطوات المتّبعة لمصادقة خادم التطبيق:

  1. يوقّع خادم التطبيق بعض معلومات JSON باستخدام مفتاح التطبيق الخاص.
  2. يتم إرسال هذه المعلومات الموقّعة إلى خدمة الإشعارات الفورية كعنوان في طلب POST.
  3. تستخدم خدمة الإشعارات الفورية المفتاح العام المخزَّن الذي تلقّته من pushManager.subscribe() للتحقّق من أنّ المعلومات التي تم تلقّيها موقَّعة بالمفتاح الخاص المرتبط بالمفتاح العام. ملاحظة: المفتاح العام هو applicationServerKey الذي يتم تمريره إلى طلب الاشتراك.
  4. إذا كانت المعلومات الموقّعة صالحة، ترسل خدمة الإشعارات الفورية الرسالة إلى المستخدم.

في ما يلي مثال على تدفّق المعلومات هذا. (لاحظ وسيلة الإيضاح في أسفل يمين الشاشة للإشارة إلى المفتاحَين العام والخاص).

صورة توضيحية لطريقة استخدام مفتاح خادم التطبيق الخاص عند إرسال رسالة

"المعلومات الموقّعة" المُضافة إلى عنوان في الطلب هي رمز JSON المميّز للويب.

رمز JSON المميّز للويب

رمز JSON المميّز للويب (أو JWT باختصار) هو طريقة لإرسال رسالة إلى جهة خارجية، ما يتيح للمستلِم التحقّق من هوية المرسِل.

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

تتوفّر مجموعة من المكتبات على jwt.io/‎ التي يمكنها تنفيذ عملية التوقيع نيابةً عنك، وننصحك باستخدامها حيثما أمكن ذلك. لإكمال الصورة، لنلقِ نظرة على كيفية إنشاء رمز JWT موقَّع يدويًا.

إشعارات الدفع على الويب ورموز JWT الموقّعة

إنّ رمز JWT الموقّع هو مجرد سلسلة، على الرغم من أنّه يمكن اعتباره ثلاث سلاسل مرتبطة ببعضها البعض بنقاط.

صورة توضيحية للسلاسل في رمز JSON المميّز للويب

السلسلتان الأولى والثانية (معلومات JWT وبيانات JWT) هما جزءان من JSON تم تشفيرهما باستخدام base64، ما يعني أنّه يمكن قراءتهما بشكل علني.

السلسلة الأولى هي معلومات حول رمز JWT المميّز نفسه، وتشير إلى الخوارزمية التي تم استخدامها لإنشاء التوقيع.

يجب أن تتضمّن معلومات JWT لإشعارات الويب الفورية المعلومات التالية:

{
  "typ": "JWT",
  "alg": "ES256"
}

السلسلة الثانية هي بيانات JWT. يوفّر ذلك معلومات حول مرسل رمز JWT والجهة التي تم إرساله إليها ومدة صلاحيته.

بالنسبة إلى الإشعارات الفورية على الويب، سيكون تنسيق البيانات على النحو التالي:

{
  "aud": "https://some-push-service.org",
  "exp": "1469618703",
  "sub": "mailto:example@web-push-book.org"
}

تمثّل قيمة aud "الجمهور"، أي الجهة التي تم إصدار رمز JWT لها. بالنسبة إلى الإشعارات الفورية على الويب، يكون الجمهور هو خدمة الإشعارات الفورية، لذا نضبطه على مصدر خدمة الإشعارات الفورية.

تمثّل القيمة exp تاريخ انتهاء صلاحية رمز JWT، ما يمنع المتطفلين من إعادة استخدام رمز JWT في حال اعتراضهم له. تكون مدة انتهاء الصلاحية طابعًا زمنيًا بالثواني ويجب ألّا تزيد عن 24 ساعة.

في Node.js، يتم ضبط تاريخ انتهاء الصلاحية باستخدام:

Math.floor(Date.now() / 1000) + 12 * 60 * 60;

يجب أن يكون الفارق 12 ساعة بدلاً من 24 ساعة لتجنُّب أي مشاكل في اختلاف التوقيت بين التطبيق المُرسِل وخدمة الإشعارات الفورية.

أخيرًا، يجب أن تكون قيمة sub إما عنوان URL أو عنوان بريد إلكتروني mailto. ويتم ذلك حتى تتمكّن خدمة الإشعارات الفورية من العثور على معلومات الاتصال الخاصة بالمرسِل من رمز JWT إذا احتاجت إلى التواصل معه. (لهذا السبب، كانت مكتبة الإشعارات الفورية على الويب بحاجة إلى عنوان بريد إلكتروني).

تمامًا مثل معلومات JWT، يتم ترميز بيانات JWT كسلسلة base64 آمنة لعنوان URL.

السلسلة الثالثة، وهي التوقيع، هي نتيجة أخذ السلسلتين الأوليين (معلومات JWT وبيانات JWT) ودمجهما باستخدام نقطة، وهو ما سنسميه "الرمز المميز غير الموقّع"، ثم توقيعه.

تتطلّب عملية التوقيع تشفير "الرمز المميز غير الموقّع" باستخدام ES256. وفقًا لمواصفات JWT، يشير ES256 إلى "خوارزمية ECDSA باستخدام منحنى P-256 وخوارزمية التجزئة SHA-256". باستخدام Web Crypto، يمكنك إنشاء التوقيع على النحو التالي:

// Utility function for UTF-8 encoding a string to an ArrayBuffer.
const utf8Encoder = new TextEncoder('utf-8');

// The unsigned token is the concatenation of the URL-safe base64 encoded
// header and body.
const unsignedToken = .....;

// Sign the |unsignedToken| using ES256 (SHA-256 over ECDSA).
const key = {
  kty: 'EC',
  crv: 'P-256',
  x: window.uint8ArrayToBase64Url(
    applicationServerKeys.publicKey.subarray(1, 33)),
  y: window.uint8ArrayToBase64Url(
    applicationServerKeys.publicKey.subarray(33, 65)),
  d: window.uint8ArrayToBase64Url(applicationServerKeys.privateKey),
};

// Sign the |unsignedToken| with the server's private key to generate
// the signature.
return crypto.subtle.importKey('jwk', key, {
  name: 'ECDSA', namedCurve: 'P-256',
}, true, ['sign'])
.then((key) => {
  return crypto.subtle.sign({
    name: 'ECDSA',
    hash: {
      name: 'SHA-256',
    },
  }, key, utf8Encoder.encode(unsignedToken));
})
.then((signature) => {
  console.log('Signature: ', signature);
});

يمكن لخدمة الإشعارات الفورية التحقّق من صحة رمز JWT المميّز باستخدام مفتاح خادم التطبيق العام لفك تشفير التوقيع والتأكّد من أنّ السلسلة التي تم فك تشفيرها هي نفسها الرمز المميز غير الموقّع (أي السلسلتان الأوليان في رمز JWT المميّز).

يتم إرسال رمز JWT الموقّع (أي السلاسل الثلاث المربوطة بنقاط) إلى خدمة الإشعارات الفورية على الويب كعنوان Authorization مع إضافة WebPush في البداية، كما يلي:

Authorization: 'WebPush [JWT Info].[JWT Data].[Signature]';

ينصّ بروتوكول Web Push أيضًا على أنّه يجب إرسال مفتاح خادم التطبيق العام في العنوان Crypto-Key كسلسلة base64 مُشفّرة وآمنة على عناوين URL مع إضافة p256ecdsa= إليها.

Crypto-Key: p256ecdsa=[URL Safe Base64 Public Application Server Key]

تشفير الحمولة

بعد ذلك، لنلقِ نظرة على كيفية إرسال حمولة مع رسالة دفع حتى يتمكّن تطبيق الويب من الوصول إلى البيانات التي يتلقّاها عند تلقّي رسالة دفع.

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

من الميزات الرائعة في الإشعارات الفورية على الويب أنّ جميع خدمات الإشعارات الفورية تستخدم واجهة برمجة التطبيقات نفسها (بروتوكول الإشعارات الفورية على الويب)، لذا لا يهم المطوّرين معرفة الجهة التي تقدّم خدمة الإشعارات الفورية. يمكننا تقديم طلب بالتنسيق الصحيح وتوقّع إرسال رسالة إشعار فوري. أما الجانب السلبي لذلك، فهو أنّ المطوّرين قد يرسلون رسائل إلى خدمة إشعارات فورية غير جديرة بالثقة. من خلال تشفير الحمولة، لا يمكن لخدمة الإشعارات الفورية قراءة البيانات التي يتم إرسالها. ولا يمكن لأي جهة سوى المتصفّح فك تشفير المعلومات. ويؤدي ذلك إلى حماية بيانات المستخدم.

يتم تحديد تشفير الحمولة في مواصفات تشفير الرسائل.

قبل أن نلقي نظرة على الخطوات المحدّدة لتشفير حمولة الرسائل الفورية، علينا أن نتناول بعض التقنيات التي سيتم استخدامها أثناء عملية التشفير. (نشكر "مات سكيلز" على مقالته الرائعة حول تشفير الإشعارات الفورية).

ECDH وHKDF

يتم استخدام كل من ECDH وHKDF في جميع مراحل عملية التشفير، وتقدّمان مزايا لأغراض تشفير المعلومات.

ECDH: تبادل مفاتيح Diffie-Hellman للمنحنى الإهليلجي

لنفترض أنّ لديك شخصَين يريدان مشاركة المعلومات، هما نبيلة ويوسف. لدى كل من نبيلة ويوسف مفتاحان عام وخاص. تشارك نبيلة ويوسف مفتاحيهما العامين مع بعضهما البعض.

إنّ الميزة المفيدة للمفاتيح التي يتم إنشاؤها باستخدام ECDH هي أنّ "أليس" يمكنها استخدام مفتاحها الخاص ومفتاح "بوب" العام لإنشاء القيمة السرية "X". يمكن لكامل إجراء الأمر نفسه، حيث يستخدم مفتاحه الخاص ومفتاح أليس العام لإنشاء القيمة نفسها "X" بشكل مستقل. وهذا يجعل "X" المفتاح السري المشترك ويجب على "أليس" و"بوب" مشاركة المفتاح العام فقط. يمكن الآن لكل من "يوسف" و"نبيلة" استخدام "X" لتشفير الرسائل وفك تشفيرها بينهما.

تحدّد خوارزمية ECDH، حسب معلوماتي، خصائص المنحنيات التي تسمح بهذه "الميزة" المتمثّلة في إنشاء مفتاح سري مشترك "X".

هذا شرح عام حول ECDH. إذا أردت معرفة المزيد، ننصحك بمشاهدة فيديو يتضمّن نظرة عامة أكثر تفصيلاً حول ECDH.

في ما يتعلق بالتعليمات البرمجية، تتضمّن معظم اللغات والمنصات مكتبات لتسهيل إنشاء هذه المفاتيح.

في العقدة، سننفّذ ما يلي:

const keyCurve = crypto.createECDH('prime256v1');
keyCurve.generateKeys();

const publicKey = keyCurve.getPublicKey();
const privateKey = keyCurve.getPrivateKey();

HKDF: دالة اشتقاق المفاتيح المستندة إلى HMAC

تقدّم Wikipedia وصفًا موجزًا HKDF:

‫HKDF هي دالة اشتقاق مفاتيح مستندة إلى HMAC تحوّل أي مادة مفتاح ضعيفة إلى مادة مفتاح قوية من الناحية التشفيرية. ويمكن استخدامه، على سبيل المثال، لتحويل الأسرار المشتركة التي تم تبادلها باستخدام Diffie Hellman إلى مادة أساسية مناسبة للاستخدام في التشفير أو التحقّق من السلامة أو المصادقة.

بشكل أساسي، ستأخذ HKDF مدخلاً غير آمن بشكل خاص وتجعله أكثر أمانًا.

تتطلّب المواصفات التي تحدّد هذا التشفير استخدام SHA-256 كخوارزمية تجزئة، ويجب ألا يتجاوز طول المفاتيح الناتجة عن HKDF في الإشعارات الفورية على الويب 256 بت (32 بايت).

في العقدة، يمكن تنفيذ ذلك على النحو التالي:

// Simplified HKDF, returning keys up to 32 bytes long
function hkdf(salt, ikm, info, length) {
  // Extract
  const keyHmac = crypto.createHmac('sha256', salt);
  keyHmac.update(ikm);
  const key = keyHmac.digest();

  // Expand
  const infoHmac = crypto.createHmac('sha256', key);
  infoHmac.update(info);

  // A one byte long buffer containing only 0x01
  const ONE_BUFFER = new Buffer(1).fill(1);
  infoHmac.update(ONE_BUFFER);

  return infoHmac.digest().slice(0, length);
}

نشكر Mat Scale على مقالته التي استندنا إليها في هذا الرمز النموذجي.

يشمل ذلك بشكل عام ECDH وHKDF.

‫ECDH هي طريقة آمنة لمشاركة المفاتيح العامة وإنشاء المفتاح السري المشترك. ‫HKDF هي طريقة لتحويل مواد غير آمنة إلى مواد آمنة.

سيتم استخدام هذا المفتاح أثناء تشفير الحمولة. لنلقِ نظرة الآن على البيانات التي نستخدمها كمدخلات وكيفية تشفيرها.

الإدخالات

عندما نريد إرسال رسالة دفع إلى مستخدم مع حمولة، نحتاج إلى ثلاثة مدخلات:

  1. حمولة البيانات نفسها
  2. auth السرّي من PushSubscription
  3. مفتاح p256dh من PushSubscription

لقد لاحظنا أنّه يتم استرداد القيمتَين auth وp256dh من PushSubscription، ولكن للتذكير السريع، نحتاج إلى القيم التالية عند تقديم اشتراك:

subscription.toJSON().keys.auth;
subscription.toJSON().keys.p256dh;

subscription.getKey('auth');
subscription.getKey('p256dh');

يجب التعامل مع قيمة auth على أنّها سرّية وعدم مشاركتها خارج تطبيقك.

المفتاح p256dh هو مفتاح عام، ويُشار إليه أحيانًا باسم المفتاح العام للعميل. سنشير هنا إلى p256dh على أنّه المفتاح العام للاشتراك. يتم إنشاء المفتاح العام للاشتراك بواسطة المتصفّح. سيحافظ المتصفّح على سرية المفتاح الخاص وسيستخدمه لفك تشفير الحمولة.

نحتاج إلى القيم الثلاث auth وp256dh وpayload كمدخلات، وسيكون الناتج من عملية التشفير هو الحمولة المشفّرة وقيمة عشوائية ومفتاح عام يُستخدَم فقط لتشفير البيانات.

Salt

يجب أن تكون القيمة العشوائية التي يتم استخدامها عبارة عن 16 بايت من البيانات العشوائية. في NodeJS، سننفّذ ما يلي لإنشاء قيمة salt:

const salt = crypto.randomBytes(16);

المفاتيح العامة والخاصة

يجب إنشاء المفتاحَين العام والخاص باستخدام منحنى قطع ناقص P-256، وهو ما سنفعله في Node على النحو التالي:

const localKeysCurve = crypto.createECDH('prime256v1');
localKeysCurve.generateKeys();

const localPublicKey = localKeysCurve.getPublicKey();
const localPrivateKey = localKeysCurve.getPrivateKey();

سنشير إلى هذه المفاتيح باسم "المفاتيح المحلية". وهي تُستخدم فقط للتشفير ولا ترتبط بأي شكل بمفاتيح خادم التطبيق.

باستخدام الحمولة وسر المصادقة والمفتاح العام للاشتراك كمدخلات، وباستخدام قيمة salt تم إنشاؤها حديثًا ومجموعة من المفاتيح المحلية، نكون مستعدين لإجراء بعض عمليات التشفير.

المفتاح السري المشترك

تتمثل الخطوة الأولى في إنشاء سر مشترك باستخدام المفتاح العام للاشتراك والمفتاح الخاص الجديد (هل تتذكر شرح ECDH مع "أليس" و"بوب"؟ تمامًا مثل ذلك).

const sharedSecret = localKeysCurve.computeSecret(
  subscription.keys.p256dh,
  'base64',
);

يُستخدم هذا الرمز في الخطوة التالية لحساب المفتاح العشوائي الزائف (PRK).

مفتاح عشوائي زائف

مفتاح التشفير العشوائي الزائف (PRK) هو مزيج من سر مصادقة الاشتراك في الإشعارات الفورية والمفتاح السري المشترك الذي أنشأناه للتو.

const authEncBuff = new Buffer('Content-Encoding: auth\0', 'utf8');
const prk = hkdf(subscription.keys.auth, sharedSecret, authEncBuff, 32);

قد تتساءل عن الغرض من السلسلة Content-Encoding: auth\0. باختصار، ليس لهذا الحقل غرض واضح، على الرغم من أنّه يمكن للمتصفّحات فك تشفير رسالة واردة والبحث عن ترميز المحتوى المتوقّع. تضيف \0 بايتًا بقيمة 0 إلى نهاية المخزن المؤقت. هذا السلوك متوقّع من المتصفّحات التي تفك تشفير الرسالة، إذ تتوقّع عددًا كبيرًا من البايتات لترميز المحتوى، يليه بايت بالقيمة 0، ثم البيانات المشفّرة.

مفتاحنا العشوائي الزائف هو ببساطة تشغيل المصادقة والمفتاح السري المشترك وجزء من معلومات الترميز من خلال HKDF (أي جعله أقوى من ناحية التشفير).

السياق

"السياق" هو مجموعة من وحدات البايت تُستخدَم لاحقًا في عملية التشفير في المتصفّح لاحتساب قيمتَين. وهو في الأساس مجموعة من وحدات البايت التي تحتوي على المفتاح العام للاشتراك والمفتاح العام المحلي.

const keyLabel = new Buffer('P-256\0', 'utf8');

// Convert subscription public key into a buffer.
const subscriptionPubKey = new Buffer(subscription.keys.p256dh, 'base64');

const subscriptionPubKeyLength = new Uint8Array(2);
subscriptionPubKeyLength[0] = 0;
subscriptionPubKeyLength[1] = subscriptionPubKey.length;

const localPublicKeyLength = new Uint8Array(2);
subscriptionPubKeyLength[0] = 0;
subscriptionPubKeyLength[1] = localPublicKey.length;

const contextBuffer = Buffer.concat([
  keyLabel,
  subscriptionPubKeyLength.buffer,
  subscriptionPubKey,
  localPublicKeyLength.buffer,
  localPublicKey,
]);

مخزن السياق النهائي هو تصنيف، وعدد وحدات البايت في المفتاح العام للاشتراك، متبوعًا بالمفتاح نفسه، ثم عدد وحدات البايت في المفتاح العام المحلي، متبوعًا بالمفتاح نفسه.

باستخدام قيمة السياق هذه، يمكننا استخدامها في إنشاء رقم خاص ومفتاح تشفير المحتوى (CEK).

مفتاح تشفير المحتوى والرقم الخاص

الرقم الخاص هو قيمة تمنع هجمات إعادة الإرسال لأنّه يجب استخدامه مرة واحدة فقط.

مفتاح تشفير المحتوى (CEK) هو المفتاح الذي سيتم استخدامه في النهاية لتشفير الحمولة.

أولاً، علينا إنشاء وحدات بايت من البيانات الخاصة بالرقم الخاص لمرة واحدة ومفتاح تشفير المحتوى (CEK)، وهو ببساطة سلسلة ترميز المحتوى متبوعة بمخزن السياق المؤقت الذي حسبناه للتو:

const nonceEncBuffer = new Buffer('Content-Encoding: nonce\0', 'utf8');
const nonceInfo = Buffer.concat([nonceEncBuffer, contextBuffer]);

const cekEncBuffer = new Buffer('Content-Encoding: aesgcm\0');
const cekInfo = Buffer.concat([cekEncBuffer, contextBuffer]);

يتم تمرير هذه المعلومات من خلال HKDF التي تجمع بين القيمة العشوائية ومفتاح الجذر الأولي مع nonceInfo وcekInfo:

// The nonce should be 12 bytes long
const nonce = hkdf(salt, prk, nonceInfo, 12);

// The CEK should be 16 bytes long
const contentEncryptionKey = hkdf(salt, prk, cekInfo, 16);

يمنحنا ذلك الرقم الخاص ومفتاح تشفير المحتوى.

تنفيذ التشفير

بعد الحصول على مفتاح تشفير المحتوى، يمكننا تشفير الحمولة.

ننشئ تشفير AES128 باستخدام مفتاح تشفير المحتوى كمفتاح، ويكون الرقم الخاص هو متّجه الإعداد.

في Node، يتم ذلك على النحو التالي:

const cipher = crypto.createCipheriv(
  'id-aes128-GCM',
  contentEncryptionKey,
  nonce,
);

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

يجب إضافة بايتَين من المساحة المتروكة للإشارة إلى طول أي مساحة متروكة إضافية.

على سبيل المثال، إذا لم تتم إضافة أي مساحة متروكة، سيكون لديك بايتان بقيمة 0، أي أنّه لا تتوفّر أي مساحة متروكة، وبعد هذين البايتين، ستتم قراءة الحمولة. إذا أضفت 5 بايت من المساحة المتروكة، سيكون للقيمة الأولى بايتان بقيمة 5، وبالتالي سيقرأ المستهلك بعد ذلك خمسة بايت إضافية ثم يبدأ في قراءة الحمولة.

const padding = new Buffer(2 + paddingLength);
// The buffer must be only zeros, except the length
padding.fill(0);
padding.writeUInt16BE(paddingLength, 0);

بعد ذلك، نمرّر الحشو والحِمل خلال هذا التشفير.

const result = cipher.update(Buffer.concat(padding, payload));
cipher.final();

// Append the auth tag to the result -
// https://nodejs.org/api/crypto.html#crypto_cipher_getauthtag
const encryptedPayload = Buffer.concat([result, cipher.getAuthTag()]);

لدينا الآن الحمولة المشفّرة. رائع

كل ما تبقى هو تحديد كيفية إرسال هذه الحمولة إلى خدمة الإشعارات الفورية.

عناوين ونص الحمولة المشفّرة

لإرسال حمولة البيانات المشفّرة هذه إلى خدمة الإشعارات الفورية، علينا تحديد بعض العناوين المختلفة في طلب POST.

عنوان التشفير

يجب أن يحتوي العنوان "التشفير" على قيمة التشفير المستخدَمة لتشفير الحمولة.

يجب أن يكون التشفير الآمن لعنوان URL باستخدام base64 مكوّنًا من 16 بايت ويجب إضافته إلى عنوان التشفير على النحو التالي:

Encryption: salt=[URL Safe Base64 Encoded Salt]

عنوان Crypto-Key

لقد رأينا أنّه يتم استخدام العنوان Crypto-Key ضمن قسم "مفاتيح خادم التطبيق" لاحتواء مفتاح خادم التطبيق العام.

يُستخدَم هذا العنوان أيضًا لمشاركة المفتاح العام المحلي المستخدَم لتشفير الحمولة.

يبدو العنوان الناتج على النحو التالي:

Crypto-Key: dh=[URL Safe Base64 Encoded Local Public Key String]; p256ecdsa=[URL Safe Base64 Encoded Public Application Server Key]

عناوين نوع المحتوى وطوله وتشفيره

يمثّل العنوان Content-Length عدد وحدات البايت في الحمولة المشفّرة. العنوانان "Content-Type" و"Content-Encoding" هما قيمتان ثابتتان. يظهر ذلك أدناه.

Content-Length: [Number of Bytes in Encrypted Payload]
Content-Type: 'application/octet-stream'
Content-Encoding: 'aesgcm'

بعد ضبط هذه العناوين، علينا إرسال الحمولة المشفّرة كنص الطلب. لاحِظ أنّ قيمة Content-Type مضبوطة على application/octet-stream. ويرجع ذلك إلى أنّه يجب إرسال الحمولة المشفّرة كسلسلة من البايتات.

في NodeJS، يمكننا إجراء ذلك على النحو التالي:

const pushRequest = https.request(httpsOptions, function(pushResponse) {
pushRequest.write(encryptedPayload);
pushRequest.end();

المزيد من العناوين؟

لقد تناولنا العناوين المستخدَمة في رموز JWT / مفاتيح خادم التطبيق (أي كيفية تعريف التطبيق باستخدام خدمة الإشعارات الفورية)، كما تناولنا العناوين المستخدَمة لإرسال حمولة مشفّرة.

هناك رؤوس إضافية تستخدمها الخدمات لإجراء تغييرات على سلوك الرسائل المرسَلة. بعض هذه العناوين مطلوبة، والبعض الآخر اختياري.

عنوان TTL

مطلوب

TTL (أو مدة البقاء) هو عدد صحيح يحدّد عدد الثواني التي تريد أن تبقى فيها رسالتك الإشعارية على خدمة الإشعارات قبل تسليمها. عند انتهاء صلاحية TTL، ستتم إزالة الرسالة من قائمة انتظار خدمة الإشعارات الفورية ولن يتم تسليمها.

TTL: [Time to live in seconds]

إذا ضبطت قيمة TTL على صفر، ستحاول خدمة الإشعارات الفورية تسليم الرسالة على الفور، ولكن إذا تعذّر الوصول إلى الجهاز، ستتم إزالة رسالتك على الفور من قائمة انتظار خدمة الإشعارات الفورية.

من الناحية الفنية، يمكن لخدمة الإشعارات الفورية تقليل TTL لرسالة إشعار فوري إذا أرادت ذلك. يمكنك معرفة ما إذا حدث ذلك من خلال فحص العنوان TTL في الردّ الوارد من خدمة الإشعارات الفورية.

الموضوع

اختياريّ

المواضيع هي سلاسل يمكن استخدامها لاستبدال الرسائل المعلقة برسالة جديدة إذا كانت تتضمّن أسماء مواضيع متطابقة.

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

حاجة ماسة

اختياريّ

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

يتم تحديد قيمة العنوان على النحو الموضّح أدناه. القيمة التلقائية هي normal.

Urgency: [very-low | low | normal | high]

كل شيء معًا

إذا كانت لديك أسئلة أخرى حول طريقة عمل كل ذلك، يمكنك دائمًا الاطّلاع على كيفية تفعيل المكتبات للرسائل الفورية على الموقع الإلكتروني web-push-libs org.

بعد الحصول على حمولة مشفّرة والعناوين المذكورة أعلاه، ما عليك سوى إرسال طلب POST إلى endpoint في PushSubscription.

إذًا، ماذا نفعل بالردّ على طلب POST هذا؟

ردّ من خدمة الإشعارات الفورية

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

رمز الحالة الوصف
201 تم الإنشاء. تم استلام طلب إرسال رسالة فورية وقبوله.
429 عدد الطلبات كبير جدًا. وهذا يعني أنّ خادم التطبيق قد بلغ الحدّ الأقصى لمعدّل إرسال البيانات إلى إحدى الخدمات. يجب أن تتضمّن خدمة الإشعارات الفورية عنوان "Retry-After" لتحديد المدة التي يجب الانتظار خلالها قبل إمكانية تقديم طلب آخر.
400 الطلب غير صالح. يعني هذا بشكل عام أنّ أحد العناوين غير صالح أو تم تنسيقه بشكل غير صحيح.
404 غير موجودة يشير ذلك إلى أنّ الاشتراك منتهي الصلاحية ولا يمكن استخدامه. في هذه الحالة، عليك حذف `PushSubscription` وانتظار أن يعيد العميل اشتراك المستخدم.
410 تمت إزالة المحتوى. لم يعُد الاشتراك صالحًا ويجب إزالته من خادم التطبيق. يمكن إعادة إنتاج هذا الخطأ من خلال استدعاء `unsubscribe()` على `PushSubscription`.
413 حجم الحمولة كبير جدًا. الحد الأدنى لحجم حمولة الخدمة التي يجب أن تتيحها خدمة الإشعارات الفورية هو 4096 بايت (أو 4 كيلوبايت).

يمكنك أيضًا الاطّلاع على معيار Web Push‏ (RFC8030) للحصول على مزيد من المعلومات حول رموز حالة HTTP.

الخطوات التالية

دروس تطبيقية حول الترميز