IlmHamroh
JavaScript Full-stack/29-qism. Backend integratsiyalar: fayl, to'lov, bot, realtime17/69-dars20 daqiqa
Mundarija (28)

Push bildirishnomalar serverda: Web Push, VAPID va FCM

Qisqacha: Server push'ni telefon yoki brauzerga emas, obunadagi endpoint manziliga — brauzer kompaniyasining push xizmatiga (Chrome — FCM, Firefox — Mozilla) yuboradi. Node'da buni web-push paketi qiladi: xabarni brauzer kalitlari bilan shifrlaydi (aes128gcm, +103 bayt) va VAPID yopiq kaliti bilan imzolaydi (ES256 JWT). Javobga qarab: 201 — qabul qilindi, 404/410 — obuna o'lgan, bazadan o'chiring, 429/5xx — keyinroq qayta. Mobil ilovalar uchun Firebase Admin SDK (FCM) — alohida yo'l, u kanonda sinalmagan.

Bu darsda

  • Push oqimining server qismini va push xizmatining rolini tushuntirasiz.
  • VAPID JWT'ni Web Crypto bilan o'zingiz yasab, imzosini tekshirasiz.
  • Shifrlash nega xabarga aniq 103 bayt qo'shishini va yuk limitini hisoblaysiz.
  • Push xizmati javobini to'rt holatga ajratasiz va eskirgan obunani tozalaysiz.
  • Kanon webpush.ts modulini lokal soxta push xizmati bilan sinaysiz.

Oldin bilishingiz kerak: Web push bildirishnomalar (brauzer tomoni: ruxsat, subscribe, Service Worker), crypto: hash, shifrlash va imzo, JWT anatomiyasi (28-qism), SMS provayderlar: Eskiz va PlayMobile.

Web push bildirishnomalar darsida brauzer tomonini qurdingiz: ruxsat so'rash, obuna olish, Service Worker'da push hodisasi. O'sha dars halol aytgan edi: push'ni serversiz yetkazib bo'lmaydi va bu qism "server darsida" bo'ladi. Mana o'sha dars.

Diqqat: Haqiqiy push xizmatlari (Chrome → FCM, Firefox → Mozilla autopush), Firebase Admin SDK va Expo push kanonda sinalmagan (2026-10-27): Google loyihasi va kalit kerak. Kanon o'rniga brauzer kabi kalit yaratadigan, imzoni tekshiradigan va xabarni haqiqatan ochadigan lokal soxta push xizmati yozdi.

1. Nega bu kerak?

Email — sekin o'qiladi, SMS — pullik. Push esa bepul va darhol: Aziz brauzerni yopgan bo'lsa ham, "Non olish — 18:00" bildirishnomasi ekran burchagida chiqadi.

Server push yuborganda uch muammoga duch keladi:

  1. Kimga yuborish? Foydalanuvchining IP'si yo'q, brauzeri uxlab yotibdi. Server faqat obunani biladi.
  2. Ishonch. Push xizmati begona serverdan kelgan xabarni nega yetkazsin? Spamchi ham yuborishi mumkin.
  3. Maxfiylik. Xabar push xizmati (Google, Mozilla) orqali o'tadi. Ular matnni o'qimasligi kerak.

O'xshatish: pochta qutisi (abonent qutisi). Aziz pochtadan quti oldi va kalitning nusxasini sizga berdi. Siz xatni qutiga solasiz, lekin xat qulfli qutichada — kalit faqat Azizda (shifrlash). Konvertda sizning muhringiz bor (VAPID imzo) — pochta xat haqiqatan sizdan ekanini tekshiradi.

2. Server qismi

2.1 Obuna ichida nima bor

Brauzer pushManager.subscribe dan keyin serverga shunday JSON beradi (Web push darsida ko'rgansiz):

json
{
  "endpoint": "https://fcm.googleapis.com/fcm/send/abc…",
  "keys": {
    "p256dh": "BEKr…",
    "auth": "k3T0…"
  }
}
Maydon Nima Server nima qiladi
endpoint push xizmatidagi shu brauzer "qutisi" xabarni shu URL'ga POST qiladi
p256dh brauzerning ochiq ECDH kaliti (P-256, 65 bayt) xabarni shifrlash uchun
auth 16 baytli maxfiy son shifrlash kalitiga qo'shiladi

Server obunani bazada saqlaydi. Bir foydalanuvchining bir nechta obunasi bo'lishi mumkin: telefon, ish kompyuteri, uy kompyuteri. Kanon jadvali push_obunalar da endpoint noyob (UNIQUE) va faqat https:// bilan boshlanadi — buni Bildirishnoma tizimi arxitekturasi darsida ko'ramiz.

2.2 Yuborish oqimi

sequenceDiagram
  participant S as Server (web-push)
  participant P as Push xizmati (FCM)
  participant B as Brauzer (SW)
  S->>S: shifrlash (p256dh, auth)
  S->>S: VAPID JWT imzosi
  S->>P: POST endpoint, TTL, Urgency
  P-->>S: 201 Created
  P->>B: xabar (brauzer uyg'onadi)
  B->>B: ochish va showNotification

Diagrammaga qarang: server faqat push xizmatini biladi va 201 olgach ishini tugatadi. Brauzer o'chiq bo'lsa, xizmat xabarni TTL soniya saqlab turadi.

2.3 web-push paketi

bash
npm i web-push@3.6.7
js
import webpush from "web-push";

// bir marta: kalitlar; ochiq — brauzerga, yopiq — .env ga
const { publicKey, privateKey } = webpush.generateVAPIDKeys();

const javob = await webpush.sendNotification(
  obuna, // bazadan: { endpoint, keys: { p256dh, auth } }
  JSON.stringify({ sarlavha: "Eslatma", matn: "Non olish — 18:00" }),
  {
    vapidDetails: {
      subject: "mailto:admin@vazifalar.example.uz",
      publicKey: process.env.VAPID_OCHIQ,
      privateKey: process.env.VAPID_YOPIQ,
    },
    TTL: 3600,
    urgency: "normal",
    topic: "vazifa-42",
  },
);
console.log(javob.statusCode); // 201

Bu bitta chaqiruv ichida uch ish bor: shifrlash, imzolash va HTTPS so'rov. Keyingi ikki bo'limda ularni ochamiz.

3. VAPID: "bu xabarni men yubordim"

3.1 JWT'ni qo'lda yasash

VAPID (RFC 8292) — server o'zini push xizmatiga tanitadigan usul. Har so'rovda Authorization: vapid t=<JWT>, k=<ochiq kalit> sarlavhasi boradi. JWT — uch qism: sarlavha, tana (aud — push xizmati manzili, exp — muddat, sub — aloqa uchun manzil) va ES256 imzo (JWT anatomiyasi 28-qismda chuqur). Uni brauzerdagi Web Crypto bilan o'zimiz yasaymiz:

js
const b64u = (baytlar) =>
  btoa(String.fromCharCode(...new Uint8Array(baytlar)))
    .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const matn = (obj) =>
  b64u(new TextEncoder().encode(JSON.stringify(obj)));

const kalit = await crypto.subtle.generateKey(
  { name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"],
);
const ochiq = await crypto.subtle.exportKey("raw", kalit.publicKey);

const sarlavha = matn({ typ: "JWT", alg: "ES256" });
const tana = matn({
  aud: "https://fcm.googleapis.com",
  exp: 1793174400,
  sub: "mailto:admin@vazifalar.example.uz",
});
const imzo = await crypto.subtle.sign(
  { name: "ECDSA", hash: "SHA-256" }, kalit.privateKey,
  new TextEncoder().encode(`${sarlavha}.${tana}`),
);
const jwt = `${sarlavha}.${tana}.${b64u(imzo)}`;

console.log(ochiq.byteLength, new Uint8Array(ochiq)[0]);
console.log(imzo.byteLength);
console.log(jwt.split(".").map((q) => q.length));
const ochiqTana = tana.replace(/-/g, "+").replace(/_/g, "/");
console.log(JSON.parse(atob(ochiqTana)));
const togri = await crypto.subtle.verify(
  { name: "ECDSA", hash: "SHA-256" }, kalit.publicKey, imzo,
  new TextEncoder().encode(`${sarlavha}.${tana}`),
);
console.log("imzo to'g'ri:", togri);

Konsolda:

text
65 4
64
[ 36, 127, 86 ]
{
  aud: 'https://fcm.googleapis.com',
  exp: 1793174400,
  sub: 'mailto:admin@vazifalar.example.uz'
}
imzo to'g'ri: true

Kalit har safar yangi, shuning uchun faqat uzunliklarni chiqardik:

  • Ochiq kalit 65 bayt, birinchi bayti 4 — "siqilmagan" nuqta (x va y koordinata, har biri 32 bayt). Bu brauzerdagi applicationServerKey.
  • ES256 imzo — 64 bayt (r va s, har biri 32). Base64url'da 86 belgi.
  • aud — push xizmatining origin'i (yo'lsiz). Kanon soxta xizmati aynan buni tekshiradi: aud boshqa bo'lsa — 403.
  • exp — 24 soatdan uzoq bo'lmasin; kanon xizmati buni ham tekshiradi.

3.2 Nima uchun kerak

Push xizmati imzoni k= dagi ochiq kalit bilan tekshiradi va bu kalitni obuna yaratilgandagi applicationServerKey bilan solishtiradi. Mos bo'lsa — xabar shu obunaning egasidan. Demak endpoint URL'ini o'g'irlagan begona odam yopiq kalitsiz unga xabar yubora olmaydi. Yopiq kalit sizsa — teskarisi: hujumchi hamma obunachilaringizga sizning nomingizdan yozadi.

Kanon testi qiziq natijani ham qayd etdi: "boshqa VAPID kaliti — xizmat rad etmaydi". Kanonning soxta xizmati kalitni obuna bilan bog'lamaydi (u faqat imzo to'g'riligini tekshiradi). Haqiqiy xizmatlarda bog'lanish bor — bu hujjat bo'yicha, sinalmagan.

4. Shifrlash: +103 bayt

4.1 Nima bo'ladi

Xabar tanasi aes128gcm (AES shifri, 128 bitli kalit, GCM rejimi) bilan shifrlanadi (RFC 8291). Uch qadam:

  1. Server har xabar uchun yangi vaqtinchalik kalit juftligi yaratadi va brauzerning p256dh kaliti bilan umumiy sir hisoblaydi. Bu ECDH usuli: ikki tomon faqat ochiq kalitlarni almashib, bir xil sirga keladi — sirning o'zi tarmoqdan o'tmaydi.
  2. Sirga auth qo'shiladi va HKDF (sirdan kerakli uzunlikdagi kalit yasaydigan funksiya) AES kalitini chiqaradi.
  3. Shu kalit bilan tana shifrlanadi.

Push xizmati faqat shifrlangan baytlarni ko'radi. Ochish kaliti faqat brauzerda.

Kanon web-push bilan uch xil hajmdagi xabarni shifrladi. Har qatorda yuk va tana farqiga qarang:

text
yuk 36 bayt → tana 139 bayt (+103), sarlavhalar: TTL, Content-Length, Content-Type, Content-Encoding, Authorization, Urgency
yuk 526 bayt → tana 629 bayt (+103), sarlavhalar: TTL, Content-Length, Content-Type, Content-Encoding, Authorization, Urgency
yuk 3026 bayt → tana 3129 bayt (+103), sarlavhalar: TTL, Content-Length, Content-Type, Content-Encoding, Authorization, Urgency
100 ta yuborish (ECDH + HKDF + AES-GCM + ES256 JWT + HTTPS keepAlive): mediana 2.49 ms, p95 6.37 ms; qabul 100

Hajm qanday bo'lmasin — doim aynan +103 bayt.

4.2 Yuk limiti

Push xizmatlari 4096 baytgacha tanani qabul qilishi kafolatlangan (RFC 8030). 103 qayerdan kelishini hisoblaymiz:

js
const yozuv = 4096;
const sarlavha = 16 + 4 + 1 + 65; // salt, rs, idlen, kalit
const teg = 16; // AES-GCM
const ajratgich = 1;
console.log(yozuv - sarlavha - teg - ajratgich);
console.log(sarlavha + teg + ajratgich);

Konsolda:

text
3993
103
  • 86 bayt — shifrlash sarlavhasi: tasodifiy tuz (salt), yozuv hajmi, kalit uzunligi va serverning vaqtinchalik ochiq kaliti (65 bayt).
  • 16 bayt — AES-GCM autentifikatsiya tegi: kimdir baytni o'zgartirsa, ochishda bilinadi.
  • 1 bayt — to'ldirish ajratgichi.

Demak foydali yuk — 3 993 bayt. Kanon modulida MAKS_YUK = 4096 - 103. Amalda push'ga to'liq ma'lumot emas, qisqa matn va havola qo'ying ("Yangi izoh: /vazifalar/42"). Batafsilini ilova ochilganda API'dan oladi.

Tekshirib ko'ring: Xabar shifrlangan. Demak push matniga parolni tiklash kodini qo'ysa bo'ladimi?

Javob

Yo'q. Yo'lda shifrlangan, lekin manzilda — ochiq: bildirishnoma qulflangan telefon ekranida ham ko'rinadi, uni yonidagi odam o'qiydi. Shifrlash push xizmatidan himoya qiladi, ekrandan emas. Maxfiy ma'lumot push'da bo'lmasin — faqat "Yangi xabar bor".

5. Javoblar va eskirgan obunalar

5.1 To'rt holat

Push xizmati javobini kanon modul to'rt turga ajratadi. Mantiqning JS nusxasi:

js
function tasnifla(status, retryAfter) {
  if (status >= 200 && status < 300) return { tur: "yetkazildi" };
  if (status === 404 || status === 410) return { tur: "eskirgan" };
  if (status === 429 || status >= 500) {
    const s = Number(retryAfter);
    return Number.isFinite(s) && s > 0
      ? { tur: "qayta", kutishSoniya: s }
      : { tur: "qayta" };
  }
  return { tur: "xato" };
}

const holatlar = [[201], [410], [404], [429, "30"], [503], [413]];
for (const [st, ra] of holatlar) {
  console.log(st, tasnifla(st, ra));
}

Konsolda:

text
201 { tur: 'yetkazildi' }
410 { tur: 'eskirgan' }
404 { tur: 'eskirgan' }
429 { tur: 'qayta', kutishSoniya: 30 }
503 { tur: 'qayta' }
413 { tur: 'xato' }
  • 410 Gone / 404 — foydalanuvchi ruxsatni olib tashladi, brauzerni o'chirdi yoki obuna muddati tugadi. Bu obunaga boshqa hech qachon yubormang — bazadan o'chiring.
  • 429 — juda ko'p so'rov; Retry-After sarlavhasi necha soniya kutishni aytadi.
  • 413 — yuk juda katta. Qayta urinish foydasiz.

201 "yetkazildi" degani emas, "push xizmati qabul qildi" degani. Email va SMS'dagidek: topshirish va yetkazish — ikki xil voqea.

5.2 Nega o'chirish muhim

Eskirgan obunalar to'planib qoladi. Bir yildan keyin bazadagi obunalarning katta qismi o'lik bo'lishi mumkin. Har yuborishda ularga ham so'rov ketadi — vaqt, tarmoq va push xizmati oldidagi obro'. 410 kelganda darhol o'chirish — eng arzon tozalash. Kanon bildirishnoma markazi buni avtomatik qiladi: push natijasi eskirgan bo'lsa, push_obunalar dan qator o'chadi.

5.3 TTL, Urgency, Topic

Sarlavha Ma'nosi vazifalar da
TTL brauzer o'chiq bo'lsa xizmat necha soniya saqlaydi 3600 (eskirgan eslatma kerak emas)
Urgency very-low … high; telefon batareyani tejashga qaraydi normal; muddat o'tsa high
Topic bir xil mavzuli yangi xabar eskisini almashtiradi vazifa-42

Topic qulay: vazifa muddati uch marta o'zgarsa, oflayn brauzer qaytganda uchta emas, oxirgi bitta bildirishnoma keladi. Kanon testida soxta xizmat TTL: 600, Urgency: high, Topic: vazifa-42 sarlavhalarini qabul qilib, tanani ochdi va yukni aynan qaytardi.

6. Kanon topilmalari

Kanon modulni yozayotganda ikkita narsa topdi:

  1. web-push 3.6.7 faqat HTTPS'ga yuboradi. U ichida https.request ishlatadi — http://127.0.0.1 dagi lokal soxta xizmatga umuman ulana olmadi. Kanon soxta xizmatni HTTPS (o'z-o'zini imzolagan sertifikat) qilib, modulga agent parametrini qo'shdi — test o'z sertifikat markazi bilan ishlaydi. Prodda agent berilmaydi: haqiqiy xizmatlar doim HTTPS.
  2. Buzilgan obuna kaliti "qayta" deb tasniflangan edi. p256dh noto'g'ri bo'lsa, web-push so'rovni umuman yubormaydi va statusCode siz xato tashlaydi. Dastlabki kod status 0 ni tarmoq uzilishi deb, qayta qaytarardi — bunday obunaga worker abadiy qayta urinardi. Tuzatish: status === 0 faqat xatoda tarmoq kodi (ECONNRESET, ETIMEDOUT — satr code) bo'lsa qayta, aks holda xato.

Ikkinchi topilma umumiy saboq: "noma'lum xato — qayta urin" qoidasi xavfli. Qayta urinish faqat vaqtinchalik ekani aniq bo'lgan xatolar uchun.

7. FCM va mobil ilovalar

Web Push — brauzerlar uchun standart. Android va iOS ilovalari uchun esa platforma xizmatlari: FCM (Firebase Cloud Messaging) va Apple'ning APNs. Ko'p loyiha ikkalasini FCM orqali boshqaradi. Bu bo'lim Firebase hujjati bo'yicha (2026-10-28 holatiga) — kanonda sinalmagan, kod va chiqish ko'rsatilmaydi.

  • Firebase Admin SDK (firebase-admin paketi) — server tomoni. Kalit — Google service account JSON fayli (sir, .env kabi saqlanadi).
  • FCM token — mobil ilova olgan qurilma identifikatori; Web Push'dagi obunaning o'rnida.
  • Notification va data xabar. notification — tizim o'zi ko'rsatadi (ilova yopiq bo'lsa ham); data — faqat kalit-qiymat, ilova o'zi qayta ishlaydi.
  • Topic (FCM ma'nosida) — obunachilar guruhi: "yangiliklar" mavzusiga bitta yuborish hammaga ketadi. Web Push'dagi Topic sarlavhasi bilan adashtirmang.
  • Multicast — bitta xabarni ko'p tokenga bir chaqiruvda.
  • Eskirgan token — FCM "ro'yxatdan o'tmagan token" xatosini qaytaradi; Web Push'dagi 410 kabi — bazadan o'chirish.
  • Expo push — Expo (React Native) ilovalari uchun o'z xizmati, ichida FCM va APNs.

Qoida bir xil: token/obuna — bazada, eskirgani — o'chiriladi, yuborish — navbat orqali, natija — to'rt holatdan biri.

8. Hujumchi nigohi

Hujum Natijasi Himoya
obunaga endpoint: "http://169.254.169.254/…" server ichki tarmoqqa so'rov yuboradi (SSRF) faqat https, ma'lum push xizmatlari ro'yxati
VAPID yopiq kaliti sizdi hamma obunachilarga soxta xabar kalit .env da; yangisi — hamma qayta obuna
boshqa foydalanuvchi obunasini o'z hisobiga yozish begona odam push'larni oladi obuna — tizimga kirgan foydalanuvchiga, endpoint noyob
push matnida maxfiy ma'lumot qulf ekranida o'qiladi faqat qisqa matn, tafsilot — ilovada

Birinchi qator ko'pincha unutiladi. endpoint — foydalanuvchi yuborgan URL, server esa unga POST qiladi. Hujumchi brauzer o'rniga o'zi JSON yuborib, endpoint ga serveringizning ichki manzilini yozadi. Bu SSRF — 28-qismda chuqur. Kanon sxemasi endpoint ~ '^https://' bilan birinchi qatlamni qo'ydi. Ikkinchi qatlam — 2-mashqda.

9. Ko'p uchraydigan xatolar

Xato Natija To'g'risi
410 dan keyin obunani o'chirmaslik o'lik obunalar to'planadi darhol o'chirish
har xatoda qayta urinish buzilgan obunaga abadiy sikl faqat 429, 5xx, tarmoq
har deploy'da yangi VAPID kaliti hamma obunalar ishdan chiqadi kalit bir marta yaratiladi, .env da
katta yuk (rasm, butun vazifa) 413 qisqa matn va havola
TTL: 0 eslatma uchun brauzer o'chiq bo'lsa — yo'qoladi mantiqiy TTL
http lokal mock bilan web-push ulanmaydi HTTPS mock + agent

10. Mashqlar

Mashqlar kurs/mashqlar/29/17-push/ da.

1-mashq (oson): to'g'ri yoki noto'g'ri

  1. Server push'ni to'g'ridan brauzerning IP manziliga yuboradi.
  2. Push xizmati xabar matnini o'qiy olmaydi.
  3. 201 javobi — bildirishnoma foydalanuvchiga ko'rindi degani.
  4. 410 kelgan obunaga ertaga qayta urinish kerak.
  5. Shifrlash yukka doim 103 bayt qo'shadi.
Yechim
  1. Noto'g'ri — endpoint ga, ya'ni push xizmatiga.
  2. To'g'ri — aes128gcm, kalit faqat brauzerda.
  3. Noto'g'ri — xizmat qabul qildi; brauzer o'chiq bo'lsa TTL davomida kutadi.
  4. Noto'g'ri — obuna o'lgan; o'chirish kerak.
  5. To'g'ri — kanon: 36, 526, 3026 baytda ham +103.

2-mashq (o'rta): obunani qabul qilishdan oldin tekshirish

obunaTekshir(ob) yozing: "ok" yoki sabab qaytarsin. Qoidalar: endpoint URL bo'lsin (new URL), protokol https:, host ma'lum xizmatlardan biri (fcm.googleapis.com, updates.push.services.mozilla.com, web.push.apple.com yoki .notify.windows.com bilan tugaydi); p256dh base64url'dan ochilganda 65 bayt va birinchisi 4; auth — 16 bayt. Ishora: base64url → base64 — - → +, _ → /, keyin atob (URL va URLSearchParams).

Yechim
js
const XIZMATLAR = [
  "fcm.googleapis.com",
  "updates.push.services.mozilla.com",
  "web.push.apple.com",
];
const baytlar = (b64u) =>
  Uint8Array.from(atob(b64u.replace(/-/g, "+").replace(/_/g, "/")),
    (c) => c.charCodeAt(0));

function obunaTekshir(ob) {
  let url;
  try {
    url = new URL(ob.endpoint);
  } catch {
    return "endpoint URL emas";
  }
  if (url.protocol !== "https:") return "faqat https";
  const host = url.hostname;
  const tanish = XIZMATLAR.includes(host)
    || host.endsWith(".notify.windows.com");
  if (!tanish) return `notanish xizmat: ${host}`;
  try {
    const p = baytlar(ob.keys.p256dh);
    if (p.length !== 65 || p[0] !== 4) return "p256dh noto'g'ri";
    if (baytlar(ob.keys.auth).length !== 16) return "auth noto'g'ri";
  } catch {
    return "kalit base64url emas";
  }
  return "ok";
}

const p256dh = btoa(String.fromCharCode(4, ...new Array(64).fill(7)))
  .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const auth = "AAAAAAAAAAAAAAAAAAAAAA";
const fcm = "https://fcm.googleapis.com/fcm/send/abc";
console.log(obunaTekshir({ endpoint: fcm, keys: { p256dh, auth } }));
console.log(obunaTekshir({
  endpoint: "http://169.254.169.254/latest", keys: { p256dh, auth },
}));
console.log(obunaTekshir({
  endpoint: "https://ichki.example.uz/x", keys: { p256dh, auth },
}));
const yomonKalit = { p256dh: "AAAA", auth };
console.log(obunaTekshir({ endpoint: fcm, keys: yomonKalit }));
const togriKalit = { p256dh, auth };
console.log(obunaTekshir({ endpoint: "salom", keys: togriKalit }));

Konsolda:

text
ok
faqat https
notanish xizmat: ichki.example.uz
p256dh noto'g'ri
endpoint URL emas

Ikkinchi qator — klassik SSRF urinishi (bulut serverlarining ichki metama'lumot manzili). endsWith(".notify.windows.com") — nuqta bilan: aks holda yomonnotify.windows.com ham o'tardi. Xizmatlar ro'yxati vaqt o'tishi bilan o'zgarishi mumkin — u brauzer hujjatlaridan olinadi va yangilanadi. 22 ta A — 16 bayt (har 4 belgi 3 bayt, to'ldirishsiz).

3-mashq (qiyin): bir foydalanuvchining hamma qurilmalariga

Foydalanuvchining beshta obunasi bor. hammagaYubor(endpointlar, chegara) yozing: bir vaqtda ko'pi bilan chegara ta so'rov; natija — { yetkazildi, ochirish, qayta } ro'yxatlari (404/410 — ochirish, 429/5xx — qayta). Soxta yubor(endpoint) 20 ms kutib status qaytaradi va eng ko'p parallel so'rovni sanaydi. Ishora: umumiy navbatdan shift() bilan oladigan chegara ta "ishchi" funksiya va Promise.all (Promise kombinatorlari).

Yechim
js
const STATUS = { a: 201, b: 410, c: 503, d: 201, e: 404 };
let parallel = 0;
let eng = 0;
async function yubor(endpoint) {
  parallel++;
  eng = Math.max(eng, parallel);
  await new Promise((r) => setTimeout(r, 20));
  parallel--;
  return STATUS[endpoint];
}

async function hammagaYubor(endpointlar, chegara) {
  const natija = { yetkazildi: [], ochirish: [], qayta: [] };
  const navbat = [...endpointlar];
  async function ishchi() {
    while (navbat.length > 0) {
      const ep = navbat.shift();
      const st = await yubor(ep);
      if (st === 404 || st === 410) natija.ochirish.push(ep);
      else if (st === 429 || st >= 500) natija.qayta.push(ep);
      else natija.yetkazildi.push(ep);
    }
  }
  await Promise.all(Array.from({ length: chegara }, ishchi));
  return natija;
}

const n = await hammagaYubor(["a", "b", "c", "d", "e"], 2);
console.log(n);
console.log("eng ko'p parallel:", eng);

Konsolda:

text
{ yetkazildi: [ 'a', 'd' ], ochirish: [ 'b', 'e' ], qayta: [ 'c' ] }
eng ko'p parallel: 2

Promise.all(endpointlar.map(yubor)) beshtasini birdan yuborardi; 10 000 obunada bu 10 000 ta bir vaqtdagi so'rov. Ishchilar naqshi chegarani ushlaydi. JavaScript bitta oqimli bo'lgani uchun navbat.shift() ikki ishchi orasida "poyga" bermaydi (Event loop). Natijadagi ochirish ro'yxati — bazadan o'chirish uchun, qayta — navbatga kechiktirib qaytarish uchun.

11. Loyiha qadami: webpush.ts moduli

Kanon push'ni modullar/src/push/webpush.ts ga yig'di. Telefon uchun uzun qatorlarni bo'ldik, ma'nosi kanondagidek (web-push 3.6.7, TypeScript 6.0.3):

ts
// webpush.ts — Web Push (RFC 8030/8291/8292): VAPID bilan imzolangan,
// aes128gcm shifrlangan xabar. Natija turlari: yetkazildi / eskirgan
// obuna (404, 410 → bazadan o'chirish) / qayta urinish (429, 5xx).
// FCM (Firebase Admin) — hujjat bo'yicha, SINALMAGAN.
import type { Agent } from "node:https";
import webpush from "web-push";

export type Obuna = {
  endpoint: string;
  keys: { p256dh: string; auth: string };
};
export type PushNatija =
  | { tur: "yetkazildi"; status: number }
  | { tur: "eskirgan"; status: number }
  | { tur: "qayta"; status: number; kutishSoniya?: number }
  | { tur: "xato"; status: number; sabab: string };

// aes128gcm: 4096 bayt yozuv − 86 bayt sarlavha (salt 16 + rs 4 +
// idlen 1 + kalit 65) − 16 tag − 1 ajratgich
export const MAKS_YUK = 4096 - 103;

type Vapid = {
  subject: string;
  publicKey: string;
  privateKey: string;
};
type Xabar = {
  sarlavha: string;
  matn: string;
  url?: string;
  teg?: string;
};
type Shoshilinch = "very-low" | "low" | "normal" | "high";

export class WebPush {
  readonly #vapid: Vapid;
  readonly #agent: Agent | undefined;
  /** web-push faqat HTTPS endpoint'ga yuboradi (`https.request`);
   * `agent` — test/mock uchun o'z CA si bilan. */
  constructor(vapid: Vapid, agent?: Agent) {
    this.#vapid = vapid;
    this.#agent = agent;
  }

  static kalitlarYarat(): { publicKey: string; privateKey: string } {
    return webpush.generateVAPIDKeys();
  }

  async yubor(
    obuna: Obuna,
    xabar: Xabar,
    o: { ttl?: number; shoshilinch?: Shoshilinch } = {},
  ): Promise<PushNatija> {
    const yuk = JSON.stringify(xabar);
    const hajm = Buffer.byteLength(yuk);
    if (hajm > MAKS_YUK) {
      const sabab = `yuk ${String(hajm)} bayt > ${String(MAKS_YUK)}`;
      return { tur: "xato", status: 0, sabab };
    }
    // Topic: bir xil teg — eskisini almashtiradi (RFC 8030 §5.4)
    const teg = xabar.teg;
    const topic = teg?.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 32);
    try {
      const r = await webpush.sendNotification(obuna, yuk, {
        vapidDetails: this.#vapid,
        TTL: o.ttl ?? 3600,
        urgency: o.shoshilinch ?? "normal",
        ...(teg ? { topic } : {}),
        timeout: 10_000,
        ...(this.#agent ? { agent: this.#agent } : {}),
      });
      return { tur: "yetkazildi", status: r.statusCode };
    } catch (e) {
      const status = (e as { statusCode?: number }).statusCode ?? 0;
      // ECONNRESET, ETIMEDOUT … — vaqtinchalik
      const kod = (e as { code?: unknown }).code;
      const tarmoq = typeof kod === "string";
      if (status === 404 || status === 410) {
        return { tur: "eskirgan", status };
      }
      const vaqtinchalik =
        status === 429 || status >= 500 || (status === 0 && tarmoq);
      if (vaqtinchalik) {
        const h = (e as { headers?: Record<string, string> }).headers;
        const ra = Number(h?.["retry-after"]);
        return Number.isFinite(ra) && ra > 0
          ? { tur: "qayta", status, kutishSoniya: ra }
          : { tur: "qayta", status };
      }
      const sabab = e instanceof Error ? e.message : String(e);
      return { tur: "xato", status, sabab };
    }
  }
}
  • PushNatija — to'rt holatli birlashma tur; chaqiruvchi switch (n.tur) bilan hammasini ko'rib chiqishga majbur.
  • Yuk limiti so'rovdan oldin tekshiriladi — 413 ni kutmasdan.
  • teg → Topic: faqat ruxsat etilgan belgilar, 32 tagacha.
  • catch — darsdagi to'rt holat va kanon topilmasi (status 0 + tarmoq kodi).

Testlar — modullar/test/push.test.ts, to'rtta, lokal soxta push xizmati (test/soxta/push-xizmati.ts) bilan. U brauzer qiladigan ishni qiladi: P-256 ECDH kalit va 16 baytli auth yaratib obuna beradi. Har so'rovda VAPID JWT'ni (ES256 imzo, aud = origin, exp ≤ 24 soat) tekshiradi va tanani http_ece bilan haqiqatan ochadi. Soxta xizmat HTTPS — sertifikatni o'zimiz yaratdik (openssl, faqat 127.0.0.1). Biz ishga tushirdik (formatlangan modul nusxasi, tsc va eslint 0, Node 24.21.0):

bash
KANON_TLS=<sertifikat papkasi> node --test test/push.test.ts
text
▶ Web Push (soxta push xizmati, HTTPS)
  ✔ VAPID kalitlari: public 65 bayt (uncompressed P-256), private 32 bayt (1.4463ms)
  ✔ yuborish: xizmat JWT ni tekshiradi va aes128gcm tanani ochadi; TTL, Urgency, Topic (43.0268ms)
  ✔ 410/404 → eskirgan; 429 → qayta (Retry-After); boshqa VAPID kaliti — xizmat rad etmaydi, imzo to'g'ri (45.3359ms)
  ✔ yuk limiti va buzilgan obuna kaliti (1.3044ms)
✔ Web Push (soxta push xizmati, HTTPS) (134.5263ms)
ℹ tests 4
ℹ suites 1
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 376.4584

To'rt test o'tdi: VAPID kalit o'lchamlari, shifrlangan yukni xizmat ochgani, 410/404/429/503 tasnifi va yuk limiti bilan buzilgan kalit. Commit:

bash
git commit -m "feat(push): Web Push — VAPID, aes128gcm, eskirgan obuna"

vazifalar-api ga ulash 28-qism tugagach: obunani qabul qiladigan marshrut tizimga kirgan foydalanuvchiga bog'lanadi, yuborish esa bildirishnoma markazi orqali.

12. Real ishda

  • Web Push — yangiliklar saytlari, chat va loyiha boshqaruv ilovalari (eslatma, izoh). Server qismi deyarli doim web-push yoki uning boshqa tillardagi muqobili.
  • FCM — mobil ilovalarda amaldagi standart; Android'da boshqa yo'l amalda yo'q, iOS uchun APNs ham FCM orqali boshqariladi.
  • Tayyor xizmatlar (OneSignal, Novu) obuna saqlash va yuborishni o'zi qiladi — narxi va bog'liqligi bilan.
  • Intervyuda: "Web Push qanday ishlaydi?", "VAPID nima uchun?", "Eskirgan push tokenlarni qanday tozalaysiz?"

Xulosa

  • Server push'ni obunadagi endpoint — push xizmatiga yuboradi; brauzerni xizmat uyg'otadi.
  • VAPID — ES256 JWT: ochiq kalit 65 bayt, imzo 64 bayt; aud — xizmat origin'i, exp ≤ 24 soat.
  • aes128gcm shifrlash doim +103 bayt; foydali yuk ≤ 3 993 bayt.
  • 201 — qabul, 404/410 — obunani o'chirish, 429/5xx — qayta (Retry-After), qolgani — xato.
  • Kanon topilmalari: web-push faqat HTTPS; noma'lum xatoni "qayta" deyish — abadiy sikl.
  • endpoint — foydalanuvchi bergan URL: SSRF'ga qarshi faqat ma'lum xizmatlar.

Keyingi dars: Bildirishnoma tizimi arxitekturasi — email, SMS, push va Telegram'ni bitta markazga yig'amiz: foydalanuvchi afzalliklari, dedup, soatlik limit va ikki worker bitta xabarni ikki marta yubormasligi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!