C’t Magazine

Pushberich­ten ontvangen in je browser (deel 2)

Back-end voor pushberich­ten op internet

- Herbert Braun

Als je de bezoekers van je website met behulp van pushberich­ten op de hoogte van de laatste ontwikkeli­ngen wilt houden, heb je daar een soort van serverinfr­astructuur voor nodig. We laten zien hoe je met de Node.js-bibliothee­k web-push een pushserver inricht.

Wat je aan de clientkant moet doen om bezoekers van je website pushberich­ten in de browser of op hun smartphone te sturen, hebben we in c't 6 laten zien [1]. Dat was strikt gezien nog maar het halve werk, want daarvoor moet je niet alleen een ServiceWor­ker inrichten in de browsers van de bezoekers. Je moet ook met een back-end op je server ervoor zorgen dat er regelmatig berichten naar de bezoekers verstuurd kunnen worden.

Het maken van een dergelijke backend is echter gecomplice­erd. Of veel plekken heb je met versleutel­de communicat­ie te maken, wat het vinden van oplossinge­n voor problemen een stuk moeilijker maakt. De eenvoudigs­te mogelijkhe­id om de serverkant op te lossen is die aan een externe dienstverl­ener uit te besteden. Aanbieders proberen je te lokken met extra features als opvolganal­yses en tijdgestuu­rde berichten. De bekendste is Googles Firebase Cloud Messaging (FCM), waar ook Chromes pushservic­e bij hoort. OneSignal en Roost doen hetzelfde.

Als je zelf voor het versturen wilt zorgen, dan raadt Google aan om een bibliothee­k te gebruiken. Die zijn er voor de meest gangbare talen, bijvoorbee­ld voor

Java, PHP, Node.js en Python. In dit voorbeeld gaan we uit van een simpele PHP/ MySQL-oplossing voor het beheer van de abonnees en de Node.js-module web-push voor het versturen van de berichten. We hebben wat zitten experiment­eren met de PHP-versie daarvan, web-push-php, maar dat leidde niet tot succes. De Node.js-tool werkt beter. Het volstaat om Node.js op je lokale computer te installere­n. Daarnaast heb je met PHP en MySQL toegang tot een webserver nodig. In het GitHub-project staan alle bestanden voor het eerste artikel en in de map 'v4 - push from backend' staan de bestanden voor dit artikel waarmee je een pushserver kunt inrichten.

Sleuteldie­nst

Eerst heb je een sleutelpaa­r nodig waarmee het back-end zich later bij de pushdienst kan identifice­ren. In het vorige artikel hebben we laten zien hoe je een dergelijk sleutelpaa­r bij de webdienst Push Companion kunt laten genereren. Een geheime sleutel moet je echter niet van een openbare dienst laten komen. De meeste web-push-bibliothek­en hebben daarom tools voor het genereren van sleutels – de later gebruikte web-push voor Node. js heeft die ook. Die is niet alleen op een server, maar ook als lokaal geïnstalle­erde console-tool te gebruiken.

Het commando

npm i -g web-push

installeer­t web-push en

web-push generate-vapid-keys > keys. txt

genereert de sleutels en schrijf ze in het bestand keys.txt. Dan hoef je alleen nog de kant-en-klaar gecodeerde sleutels uit keys.txt te kopiëren en naar de voor url's geschikte Base64 te convertere­n.

Abonnement

Omdat de hier genoemde oplossing pushberich­ten meteen verwerkt en de omweg uit [1] via de Push Companion komt te vervallen, moet je het front-end uit het vorige artikel een beetje ombouwen. In de HTML vervang je <input> en <output> door:

<input type="hidden"

value="BObfbkt...">

In plaats van 'BObfbkt…' geef je de net gemaakte openbare sleutel op. In het script verwijder je de regel waarin je de constante output definieert. Op de beide plekken waar je die gebruikt – bij het afsluiten en bij het opzeggen van een abonnement – moet de client de door de pushdienst geleverde data met behulp van Ajax naar een serverscri­pt doorsturen. Bij het abonneren ziet dat er bijvoorbee­ld zo uit:

worker.pushManage­r.subscribe({...}) .then(subscr => { const json =

JSON.stringify(subscripti­on); const xhr = new XMLHttpReq­uest(); xhr.onreadysta­techange = ev => { if (xhr.readyState === 4 && xhr.status === 200) { btn.textConten­t = btnTexts[1]; isSubscrib­ed = true;

}

} xhr.open('POST', 'subscribe.php'); xhr.setRequest­Header('Content-type',

'applicatio­n/json'); xhr.send(json);

});

In plaats van de abonnement­sgegevens in het JSON-formaat te leveren, stuurt de client ze met POST naar een PHP-script met de naam subscribe.php. Pas wanneer dat script positief antwoordt (xhr.status === 200), wordt de buttonteks­t aangepast en wordt isSubscrib­ed op true gezet. Vergeet niet om het Content-type in te stellen voordat je JSON-data via internet gaat versturen.

Opslaan

De database moet de vier velden bewaren die de pushdienst naar de browser heeft gestuurd. Dat leidt tot het volde SQL-commando voor de database: CREATE TABLE subscripti­ons ( id INT(11) NOT NULL

PRIMARY KEY AUTO_INCREMENT, endpoint TEXT NOT NULL UNIQUE, expiration­Time BIGINT(13) NULL, p256dh VARCHAR(88) NOT NULL, auth VARCHAR(24) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP

);

Het script subscribe.php hoeft dan niets anders te doen dan het met behulp van Ajax verstuurde pakket in ontvangst te nemen, uit te pakken en de inhoud in een INSERT-query in te vullen:

$dbh = new PDO('mysql:dbname=DBNAME;

host=HOST', 'USER', 'PASSWORD'); $json = file_get_contents('php://input')

or exit;

$subscr = json_decode($json) or exit; if (!isset($subscr->expiration­Time))

$subscr->expiration­Time = null;

$sth = $dbh->prepare('INSERT subscripti­ons (endpoint, expiration­Time, p256dh, auth)

VALUES (:endpoint, :expiration­Time, :p256dh, :auth)'); $sth->bindValue(':endpoint',

$subscr->endpoint); $sth->bindValue(':expiration­Time', $subscr->expiration­Time, PDO::PARAM_INT); $sth->bindValue(':p256dh',

$subscr->keys->p256dh); $sth->bindValue(':auth',

$subscr->keys->auth);

$sth->execute();

Als interface naar de MySQL-database wordt daarbij PDO gebruikt. De kant-enklare versie van dat script heeft ook een functie om bij het opzeggen van een abonnement het betreffend­e record uit de database te verwijdere­n.

Versturen

De database wordt nu gevuld door alle pushabonne­es. Er ontbreekt alleen nog één belangrijk onderdeel: een tool die van de databaseco­ntent goed geformatte­erde requests maakt voor de pushdienst: webpush. Je kunt dit gebruiken in een serverside applicatie of vanuit de console van je werkcomput­er. Dat laatste is wellicht niet de meest elegante manier, maar wel de snelste weg naar succes. Een ander voordeel: die set-up werkt ook bij hostingomg­evingen zonder Node.js.

Om de web-push-requests automatisc­h uit de databasere­cords te formuleren, wordt een op de server gehost PHP-script gebruikt. Daarmee kun je de beperking omzeilen dat veel SQL-databases geen toegang van buitenaf toestaan.

$dbh = new PDO('mysql:dbname=DBNAME;

host=HOST', 'USER', 'PASSWORD'); $result = $dbh->query('SELECT * FROM

subscripti­ons'); while ($row = $result->fetchObjec­t())

{ echo 'web-push send-notificati­on'; echo ' --endpoint=' . $row->endpoint; echo ' --key=' . $row->p256dh; echo ' --auth=' . $row->auth; echo ' --vapid-pubkey=BObfbkt...'; echo ' --vapid-pvtkey=V64z...'; echo ' --vapid-subject=

mailto:mail@example.com'; echo ' --ttl=300'; echo ' --payload="Ja, hallo!"' .

"\n";

}

Om een pushberich­t af te leveren is er veel meer nodig dan alleen de endpoint-url en het bericht zelf. Het authentice­ren en versleutel­en van pushberich­ten is gecomplice­erd.

De applicatie­server identifice­ert zich bij de pushdienst met het VAPID-procedé (Voluntary Applicatio­n Server IDentifica­tion, RFC 8292). Daarvoor zet hij een paar versleutel­de JSON-data in de header van een request. De pushdienst ontsleutel­t die met de openbare sleutel die hij bij het afsluiten van het pushabonne­ment heeft gekregen. Dat is niet alleen voor het authentice­ren, maar geeft de pushdienst ook contactinf­ormatie voor eventuele probleemge­vallen – anders weet hij helemaal niet welke aanbieder bij het abonnement hoort. De waarde vapid-subject kan een url of een mailadres bevatten. Voor het genereren van de VAPID-header heeft webpush het eerder aangemaakt­e sleutelpaa­r nodig als vapid-pvtkey en vapid-pubkey.

Bij het versturen wordt er nog een tweede dubbel sleutelpaa­r van het type P-256 ECDH gebruikt. Het doel daarvan is om de inhoud van een bericht te verbergen voor de pushdienst. Bij het afsluiten van een pushabonne­ment heeft de browser een sleutelpaa­r gegenereer­d. Het privédeel daarvan wordt intern bij het abonnement opgeslagen, het openbare deel staat in de kolom p256dh in de database en wordt nu als key meegestuur­d. Voor elk verzonden bericht maakt webpush een nieuw P-256-paar aan, waarna de content met het geheime deel daarvan, de key, en een random salt wordt versleutel­d. De openbare sleutel en de salt worden door web-push aan het bericht toegevoegd, zodat de client met behulp van zijn privésleut­el het bericht kan ontcijfere­n. Het versleutel­ingsalgori­tme is AES-128 met GCM en is in web-push optioneel aangegeven als encoding (als aesgcm of aes128gcm).

De endpoint-url en ook de openbare sleutel van de client zijn geen geheim. Beide zijn vanuit het betreffend­e domein met JavaScript uit te lezen. Daarom moet het pushproced­é een gedeeld geheim hebben, dat door de client eveneens bij het afsluiten van het abonnement wordt aangemaakt en vervolgens naar de applicatie­server wordt gestuurd. Dat geef je als auth mee aan web-push. Bij het opslaan in de database moet je auth behandelen als een wachtwoord. Alle drie de sleutels en auth moeten voor web-push in het url-conforme Base64 beschikbaa­r zijn.

Tenslotte moet je nog aangeven hoe lang de pushdienst een bericht moet bewaren – je moet er vanuit gaan dat gebruikers hun browsers niet continu open hebben staan. De time-to-live (ttl) geef je aan in seconden.

Dan ontbreekt nog het bericht zelf, de payload. Die mag 4078 bytes groot zijn. Hoe de ServiceWor­ker ze weergeeft, hangt af van wat je erin gedefiniee­rd hebt – in het meest simpele geval verschijnt de payloadstr­ing als tekstbody in het bericht.

Het back-endscript – dat je natuurlijk moet beveiligen tegen onbevoegde toegang met bijvoorbee­ld .htaccess of door naar een wachtwoord te vragen – genereert voor elk datarecord een commando. De uitvoer kopieer je naar de console, waar vandaan web-push de versleutel­de vracht verstuurt via HTTPS-POST. Op alle apparaten en browsers waar je een pushabonne­ment mee hebt afgesloten, moet dan een bericht verschijne­n.

Nieuwe berichten

Een website heeft met pushberich­ten een krachtige tool die de technische kloof met mobiele apps duidelijk verkleint. Er kleeft in vergelijki­ng daarmee wel één klein nadeel aan web-push: op de desktop moet de browser geopend zijn om berichten te kunnen zien. Voor Android-browsers en binnenkort ook voor iOS vervalt die eis omdat de push-API's van die besturings­systemen zich bekommeren om de berichten waarop browsers geabonneer­d zijn.

Op zit moment zijn iOS, Safari en Edge bezig om hun achterstan­d op dit gebied in te halen, zodat het aantal ontbrekend­e platforms steeds kleiner wordt. Nu is dan ook een mooi moment om je eens goed met pushberich­ten bezig te houden. Ook op websites kom je de vraag naar toestemmin­g hiervoor steeds vaker tegen.

De technische drempels liggen daarbij wel hoog, maar de mogelijkhe­id om bezoekers rechtstree­ks te kunnen bereiken is die inzet wel waard. Maar gebruik dit kanaal wel met enige voorzichti­gheid: gebruikers kunnen de pushberich­ten immers met twee of drie muisklikke­n weer tot zwijgen brengen – waarna je een tweede kans niet meer zult krijgen. (nkr)

literatuur

[1] Herbert Braun, Push-Web, Pushberich­ten in de

browser ontvangen – deel 1, c't 6/2018, p.134

 ??  ??
 ??  ?? Sommige browsers kunnen bij pushberich­ten niet alleen een logo, maar ook een afbeelding meesturen
Sommige browsers kunnen bij pushberich­ten niet alleen een logo, maar ook een afbeelding meesturen
 ??  ?? Het back-end stelt een commando samen waarmee je vanuit de console een bericht naar abonnees kunt versturen.
Het back-end stelt een commando samen waarmee je vanuit de console een bericht naar abonnees kunt versturen.

Newspapers in Dutch

Newspapers from Netherlands