פרוטוקול Web Push

ראינו איך אפשר להשתמש בספרייה כדי להפעיל הודעות פוש, אבל מה בדיוק הספריות האלה עושות?

הם שולחים בקשות לרשת ומוודאים שהבקשות האלה הן בפורמט הנכון. המפרט שמגדיר את בקשה לאחזור מהרשת הזו הוא פרוטוקול Web Push.

דיאגרמה של שליחת הודעת דחיפה מהשרת לשירות דחיפה.

בקטע הזה מוסבר איך השרת יכול לזהות את עצמו באמצעות מפתחות של שרת האפליקציה, ואיך מטען הייעודי המוצפן והנתונים המשויכים נשלחים.

זה לא הצד היפה של הודעות פוש באינטרנט, ואני לא מומחה בהצפנה, אבל בואו נבדוק כל חלק כי כדאי לדעת מה הספריות האלה עושות מתחת לפני השטח.

מפתחות של שרת האפליקציה

כשמצרפים משתמש למינוי, אנחנו מעבירים applicationServerKey. המפתח הזה מועבר לשירות הדחיפה ומשמש לבדיקה שהאפליקציה שהמשתמש נרשם אליה היא גם האפליקציה שמפעילה את הודעות הדחיפה.

כשאנחנו מפעילים הודעת פוש, אנחנו שולחים קבוצה של כותרות שמאפשרות לשירות הפוש לאמת את האפליקציה. (ההגדרה הזו מופיעה במפרט של VAPID).

מה המשמעות של כל זה ומה קורה בפועל? אלה השלבים שצריך לבצע כדי לאמת את שרת האפליקציה:

  1. שרת האפליקציה חותם על מידע JSON מסוים באמצעות המפתח הפרטי של האפליקציה.
  2. המידע החתום הזה נשלח לשירות הדחיפה ככותרת בבקשת POST.
  3. שירות הדחיפה משתמש במפתח הציבורי המאוחסן שקיבל מ-pushManager.subscribe() כדי לבדוק שהמידע שהתקבל חתום על ידי המפתח הפרטי שקשור למפתח הציבורי. חשוב לזכור: המפתח הציבורי הוא applicationServerKeyהפרמטר שמועבר לקריאה לפונקציה subscribe.
  4. אם המידע החתום תקין, שירות הדחיפה שולח את הודעת הדחיפה למשתמש.

למטה מופיעה דוגמה לזרימת המידע הזו. (שימו לב למקרא בפינה הימנית התחתונה שמציין מפתחות ציבוריים ופרטיים).

איור שמראה איך משתמשים במפתח הפרטי של שרת האפליקציה כששולחים הודעה.

המידע החתום שנוסף לכותרת בבקשה הוא JSON Web Token.

טוקן רשת מבוסס JSON

אסימון אינטרנט מסוג JSON (או JWT בקיצור) הוא דרך לשלוח הודעה לצד שלישי כך שהמקבל יוכל לאמת מי שלח אותה.

כשצד שלישי מקבל הודעה, הוא צריך לקבל את המפתח הציבורי של השולח ולהשתמש בו כדי לאמת את החתימה של ה-JWT. אם החתימה תקפה, סימן שה-JWT נחתם באמצעות המפתח הפרטי התואם, ולכן הוא חייב להגיע מהשולח הצפוי.

יש הרבה ספריות בכתובת jwt.io/ שיכולות לבצע את החתימה בשבילך, ואני ממליץ לך לעשות את זה איפה שאפשר. לצורך השלמת התמונה, נראה איך ליצור באופן ידני JWT חתום.

הודעות פוש לאתרים ו-JWT חתום

קובץ JWT חתום הוא רק מחרוזת, אבל אפשר לחשוב עליו כמחרוזת שמורכבת משלוש מחרוזות שמחוברות באמצעות נקודות.

איור של המחרוזות ב-JSON Web Token.

המחרוזות הראשונה והשנייה (המידע על ה-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 שעות כדי למנוע בעיות בהבדלים בשעון בין האפליקציה השולחת לבין שירות ה-Push.

לבסוף, הערך של sub צריך להיות כתובת URL או כתובת אימייל mailto. כך, אם שירות דחיפה יצטרך ליצור קשר עם השולח, הוא יוכל למצוא את הפרטים ליצירת קשר ב-JWT. (זו הסיבה לכך שספריית ה-web-push דרשה כתובת אימייל).

בדומה ל-JWT Info, גם JWT Data מקודד כמחרוזת base64 בטוחה לשימוש בכתובת URL.

המחרוזת השלישית, החתימה, היא התוצאה של שתי המחרוזות הראשונות (פרטי ה-JWT ונתוני ה-JWT), שחוברו באמצעות נקודה. נקרא לזה 'האסימון הלא חתום', והוא חתום.

תהליך החתימה מחייב הצפנה של ה-token הלא חתום באמצעות ES256. לפי מפרט JWT, ‏ ES256 הוא קיצור של "ECDSA באמצעות עקומת P-256 ואלגוריתם הגיבוב (hash) 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);
});

שירות push יכול לאמת JWT באמצעות המפתח הציבורי של שרת האפליקציות כדי לפענח את החתימה ולוודא שהמחרוזת המפוענחת זהה ל'אסימון לא חתום' (כלומר, שתי המחרוזות הראשונות ב-JWT).

ה-JWT החתום (כלומר, כל שלוש המחרוזות שמחוברות באמצעות נקודות) נשלח לשירות ה-Web Push ככותרת 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]

הצפנת המטען הייעודי (Payload)

בשלב הבא נראה איך אפשר לשלוח מטען ייעודי (payload) עם הודעת פוש, כדי שכשאפליקציית האינטרנט שלנו תקבל הודעת פוש, היא תוכל לגשת לנתונים שהיא מקבלת.

שאלה נפוצה שעולה אצל מי שהשתמש בשירותי הודעות פוש אחרים היא למה צריך להצפין את מטען הייעודי (payload) של הודעות פוש באתרים? באפליקציות מקוריות, הודעות פוש יכולות לשלוח נתונים כטקסט פשוט.

אחד היתרונות של הודעות פוש באינטרנט הוא שכל שירותי הפוש משתמשים באותו API (פרוטוקול הפוש באינטרנט), ולכן מפתחים לא צריכים לדעת מי מספק את שירות הפוש. הם יכולים לשלוח בקשה בפורמט הנכון ולצפות להודעת פוש. החיסרון הוא שמפתחים יכולים לשלוח הודעות לשירות Push שלא ניתן לסמוך עליו. הצפנה של מטען הייעודי (payload) מונעת משירות הדחיפה לקרוא את הנתונים שנשלחים. רק הדפדפן יכול לפענח את המידע. כך אנחנו מגנים על נתוני המשתמש.

ההצפנה של המטען הייעודי מוגדרת במפרט של הצפנת הודעות.

לפני שנפרט את השלבים הספציפיים להצפנת מטען ייעודי (payload) של הודעות פוש, נסביר כמה טכניקות שישמשו במהלך תהליך ההצפנה. (תודה רבה למאט סקיילס (Mat Scales) על המאמר המצוין שלו בנושא הצפנה של הודעות פוש).

‫ECDH ו-HKDF

האלגוריתמים ECDH ו-HKDF משמשים לאורך תהליך ההצפנה ומציעים יתרונות לצורך הצפנת מידע.

ECDH: חילופי מפתחות ב-Elliptic Curve Diffie-Hellman

נניח שיש שני אנשים שרוצים לשתף מידע, אליס ובוב. גם למיכל וגם ליוסי יש מפתחות ציבוריים ופרטיים משלהם. מיכל ויוסי משתפים זה עם זו את המפתחות הציבוריים שלהם.

המאפיין השימושי של מפתחות שנוצרו באמצעות ECDH הוא שאליס יכולה להשתמש במפתח הפרטי שלה ובמפתח הציבורי של בוב כדי ליצור ערך סודי X. דני יכול לעשות את אותו הדבר, לקחת את המפתח הפרטי שלו ואת המפתח הציבורי של דנה כדי ליצור באופן עצמאי את אותו הערך 'X'. כך X הוא סוד משותף, ועינת וירון צריכים לשתף רק את המפתח הציבורי שלהם. עכשיו בוב ואליס יכולים להשתמש ב-X כדי להצפין ולפענח הודעות ביניהם.

לפי הידע שלי, ECDH מגדיר את המאפיינים של עקומות שמאפשרים את ה"תכונה" הזו של יצירת סוד לשימוש עם טוקן צרכן X.

זהו הסבר כללי על ECDH. אם אתם רוצים לקבל מידע נוסף, מומלץ לצפות בסרטון עם סקירה מפורטת יותר על ECDH.

מבחינת קוד, לרוב השפות או הפלטפורמות יש ספריות שמקלות על יצירת המפתחות האלה.

ב-node, מבצעים את הפעולות הבאות:

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

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

HKDF: פונקציית נגזרת מפתח מבוססת-HMAC

בוויקיפדיה יש תיאור תמציתי של HKDF:

‫HKDF היא פונקציה להפקת מפתחות שמבוססת על HMAC, והיא הופכת חומר מפתח חלש לחומר מפתח חזק מבחינה קריפטוגרפית. לדוגמה, אפשר להשתמש בו כדי להמיר סודות משותפים שהוחלפו ב-Diffie Hellman לחומר מפתח שמתאים לשימוש בהצפנה, בבדיקת שלמות או באימות.

בעצם, HKDF מקבל קלט שלא מאובטח במיוחד והופך אותו למאובטח יותר.

ההגדרה שמגדירה את ההצפנה הזו מחייבת שימוש ב-SHA-256 כאלגוריתם הגיבוב שלנו, והמפתחות שמתקבלים עבור HKDF בהודעות פוש לאינטרנט לא יכולים להיות ארוכים מ-256 ביט (32 בייט).

ב-node, אפשר להטמיע את זה כך:

// 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 היא דרך לקחת חומר לא מאובטח ולהפוך אותו למאובטח.

הערך הזה ישמש במהלך ההצפנה של מטען הייעודי (payload). עכשיו נראה מה אנחנו מקבלים כקלט ואיך הוא מוצפן.

כניסות קלט

כדי לשלוח הודעת פוש למשתמש עם מטען ייעודי (payload), צריך להזין שלוש הגדרות:

  1. המטען הייעודי (payload) עצמו.
  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 ומפתח ציבורי שמשמש רק להצפנת הנתונים.

Salt

ערך ה-salt צריך להיות 16 בייט של נתונים אקראיים. ב-NodeJS, כדי ליצור מלח, מבצעים את הפעולות הבאות:

const salt = crypto.randomBytes(16);

מפתחות ציבוריים / פרטיים

המפתחות הציבורי והפרטי צריכים להיווצר באמצעות עקומה אליפטית P-256. כך עושים את זה ב-Node:

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

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

אנחנו נתייחס למפתחות האלה כאל 'מפתחות מקומיים'. הם משמשים רק להצפנה ואין להם קשר למפתחות של שרת האפליקציות.

אחרי שמזינים את המטען הייעודי, הסוד לאימות והמפתח הציבורי של המינוי, ויוצרים מלח חדש וקבוצה של מפתחות מקומיים, אפשר להתחיל בהצפנה.

סוד לשימוש עם טוקן צרכן

השלב הראשון הוא ליצור סוד לשימוש עם טוקן צרכן באמצעות המפתח הציבורי של המינוי והמפתח הפרטי החדש שלנו (זוכרים את ההסבר על 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 לסוף ה-Buffer. הדפדפנים מצפים לכך כשהם מפענחים את ההודעה, והם מצפים לכל כך הרבה בייטים לקידוד התוכן, ואחריהם בייט עם ערך 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,
]);

מאגר ההקשר הסופי הוא תווית, מספר הבייטים במפתח הציבורי של המינוי, ואחריו המפתח עצמו, ואז מספר הבייטים של המפתח הציבורי המקומי, ואחריו המפתח עצמו.

בעזרת ערך ההקשר הזה, אפשר להשתמש בו ליצירת מספר חד-פעמי (nonce) ומפתח להצפנת תוכן (CEK).

מפתח הצפנה של תוכן וערך חד-פעמי

צופן חד-פעמי (nonce) הוא ערך שמונע מתקפות שידור חוזר, כי אפשר להשתמש בו רק פעם אחת.

המפתח להצפנת תוכן (CEK) הוא המפתח שישמש בסופו של דבר להצפנת המטען הייעודי (payload).

קודם צריך ליצור את בייטים הנתונים עבור ה-nonce וה-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 בשילוב המלח וה-PRK עם 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);

כך אנחנו מקבלים את ה-nonce ואת מפתח ההצפנה של התוכן.

ביצוע ההצפנה

עכשיו, כשיש לנו מפתח להצפנת התוכן, אנחנו יכולים להצפין את המטען הייעודי (payload).

אנחנו יוצרים צופן AES128 באמצעות מפתח הצפנה של התוכן כמפתח, וה-nonce הוא וקטור אתחול.

ב-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.

כותרת הצפנה

הכותרת 'הצפנה' צריכה להכיל את ה-salt שמשמש להצפנת מטען הייעוד.

ה-salt בגודל 16 בייט צריך לעבור קידוד בפורמט Base64 שמתאים לכתובות URL, ולהתווסף לכותרת ההצפנה, כך:

Encryption: salt=[URL Safe Base64 Encoded Salt]

כותרת Crypto-Key

הבחננו בכותרת Crypto-Key שמשמשת בקטע Application Server Keys (מפתחות שרת אפליקציות) כדי להכיל את המפתח הציבורי של שרת האפליקציות.

הכותרת הזו משמשת גם לשיתוף המפתח הציבורי המקומי שמשמש להצפנה של מטען הייעודי (payload).

הכותרת שמתקבלת נראית כך:

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 יפוג, היא תוסר מתור ההמתנה של שירות הדחיפה ולא תימסר.

TTL: [Time to live in seconds]

אם מגדירים TTL של אפס, שירות הדחיפה ינסה להעביר את ההודעה באופן מיידי, אבל אם אי אפשר להגיע למכשיר, ההודעה תימחק באופן מיידי מתור הדחיפה של שירות הדחיפה.

מבחינה טכנית, שירות Push יכול לצמצם את TTL של הודעת Push אם הוא רוצה. כדי לדעת אם זה קרה, בודקים את הכותרת TTL בתגובה משירות הדחיפה.

נושא

אופציונלי

נושאים הם מחרוזות שאפשר להשתמש בהן כדי להחליף הודעות בהמתנה בהודעה חדשה, אם יש להן שמות נושאים תואמים.

האפשרות הזו שימושית בתרחישים שבהם נשלחות כמה הודעות בזמן שהמכשיר במצב אופליין, ואתם רוצים שהמשתמש יראה רק את ההודעה האחרונה כשהמכשיר יופעל.

דחיפות

אופציונלי

הדחיפות מציינת לשירות הדחיפה עד כמה ההודעה חשובה למשתמש. שירות ה-Push יכול להשתמש בנתונים האלה כדי לחסוך בסוללה של המכשיר של המשתמש, ולהפעיל אותו רק כשמתקבלים הודעות חשובות כשהסוללה חלשה.

ערך הכותרת מוגדר כמו שמוצג בהמשך. ערך ברירת המחדל הוא normal.

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

הכול ביחד

אם יש לכם שאלות נוספות לגבי אופן הפעולה של הכול, אתם תמיד יכולים לראות איך ספריות מפעילות הודעות פוש בארגון web-push-libs.

אחרי שיש לכם מטען ייעודי מוצפן וכותרות כמו בדוגמה שלמעלה, אתם רק צריכים לשלוח בקשת POST אל endpoint ב-PushSubscription.

אז מה עושים עם התגובה לבקשת ה-POST הזו?

תגובה משירות הדחיפה

אחרי ששולחים בקשה לשירות Push, צריך לבדוק את קוד הסטטוס של התגובה כדי לדעת אם הבקשה הצליחה או לא.

קוד סטטוס תיאור
201 נוצר. הבקשה לשליחת הודעת פוש התקבלה ואושרה.
429 יותר מדי בקשות. כלומר, שרת האפליקציה הגיע להגבלת שיעור בקשות בשירות push. שירות ה-Push צריך לכלול כותרת Retry-After כדי לציין כמה זמן צריך לחכות לפני שאפשר לשלוח בקשה נוספת.
400 בקשה לא חוקית. בדרך כלל, המשמעות היא שאחת מהכותרות לא תקינה או שהפורמט שלה שגוי.
404 לא נמצא. המשמעות היא שהמינוי פג ולא ניתן להשתמש בו. במקרה כזה, צריך למחוק את `PushSubscription` ולחכות שהלקוח ירשום מחדש את המשתמש.
410 הוא נעלם. המינוי כבר לא תקף וצריך להסיר אותו משרת האפליקציות. אפשר לשחזר את הבעיה על ידי קריאה לפונקציה unsubscribe()‎ ב-PushSubscription.
413 גודל המטען הייעודי (payload) גדול מדי. גודל המטען הייעודי המינימלי ששירות Push חייב לתמוך בו הוא 4,096 בייט (או 4KB).

מידע נוסף על קודי סטטוס של HTTP זמין גם בתקן Web Push‏ (RFC8030).

לאן כדאי ללכת

Code labs