Pushberichten ontvangen in je browser (deel 2)
Back-end voor pushberichten op internet
Als je de bezoekers van je website met behulp van pushberichten op de hoogte van de laatste ontwikkelingen wilt houden, heb je daar een soort van serverinfrastructuur voor nodig. We laten zien hoe je met de Node.js-bibliotheek web-push een pushserver inricht.
Wat je aan de clientkant moet doen om bezoekers van je website pushberichten 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 ServiceWorker 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 gecompliceerd. Of veel plekken heb je met versleutelde communicatie te maken, wat het vinden van oplossingen voor problemen een stuk moeilijker maakt. De eenvoudigste mogelijkheid om de serverkant op te lossen is die aan een externe dienstverlener uit te besteden. Aanbieders proberen je te lokken met extra features als opvolganalyses en tijdgestuurde berichten. De bekendste is Googles Firebase Cloud Messaging (FCM), waar ook Chromes pushservice bij hoort. OneSignal en Roost doen hetzelfde.
Als je zelf voor het versturen wilt zorgen, dan raadt Google aan om een bibliotheek te gebruiken. Die zijn er voor de meest gangbare talen, bijvoorbeeld 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 experimenteren 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 installeren. 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.
Sleuteldienst
Eerst heb je een sleutelpaar nodig waarmee het back-end zich later bij de pushdienst kan identificeren. In het vorige artikel hebben we laten zien hoe je een dergelijk sleutelpaar 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-bibliotheken 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ïnstalleerde console-tool te gebruiken.
Het commando
npm i -g web-push
installeert 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 converteren.
Abonnement
Omdat de hier genoemde oplossing pushberichten 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 serverscript doorsturen. Bij het abonneren ziet dat er bijvoorbeeld zo uit:
worker.pushManager.subscribe({...}) .then(subscr => { const json =
JSON.stringify(subscription); const xhr = new XMLHttpRequest(); xhr.onreadystatechange = ev => { if (xhr.readyState === 4 && xhr.status === 200) { btn.textContent = btnTexts[1]; isSubscribed = true;
}
} xhr.open('POST', 'subscribe.php'); xhr.setRequestHeader('Content-type',
'application/json'); xhr.send(json);
});
In plaats van de abonnementsgegevens 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 buttontekst aangepast en wordt isSubscribed 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 subscriptions ( id INT(11) NOT NULL
PRIMARY KEY AUTO_INCREMENT, endpoint TEXT NOT NULL UNIQUE, expirationTime 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->expirationTime))
$subscr->expirationTime = null;
$sth = $dbh->prepare('INSERT subscriptions (endpoint, expirationTime, p256dh, auth)
VALUES (:endpoint, :expirationTime, :p256dh, :auth)'); $sth->bindValue(':endpoint',
$subscr->endpoint); $sth->bindValue(':expirationTime', $subscr->expirationTime, 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 betreffende record uit de database te verwijderen.
Versturen
De database wordt nu gevuld door alle pushabonnees. Er ontbreekt alleen nog één belangrijk onderdeel: een tool die van de databasecontent goed geformatteerde requests maakt voor de pushdienst: webpush. Je kunt dit gebruiken in een serverside applicatie of vanuit de console van je werkcomputer. 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 hostingomgevingen zonder Node.js.
Om de web-push-requests automatisch uit de databaserecords 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
subscriptions'); while ($row = $result->fetchObject())
{ echo 'web-push send-notification'; 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 pushbericht af te leveren is er veel meer nodig dan alleen de endpoint-url en het bericht zelf. Het authenticeren en versleutelen van pushberichten is gecompliceerd.
De applicatieserver identificeert zich bij de pushdienst met het VAPID-procedé (Voluntary Application Server IDentification, RFC 8292). Daarvoor zet hij een paar versleutelde JSON-data in de header van een request. De pushdienst ontsleutelt die met de openbare sleutel die hij bij het afsluiten van het pushabonnement heeft gekregen. Dat is niet alleen voor het authenticeren, maar geeft de pushdienst ook contactinformatie voor eventuele probleemgevallen – 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 aangemaakte sleutelpaar nodig als vapid-pvtkey en vapid-pubkey.
Bij het versturen wordt er nog een tweede dubbel sleutelpaar 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 pushabonnement heeft de browser een sleutelpaar gegenereerd. 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 meegestuurd. 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 versleuteld. De openbare sleutel en de salt worden door web-push aan het bericht toegevoegd, zodat de client met behulp van zijn privésleutel het bericht kan ontcijferen. Het versleutelingsalgoritme 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 betreffende 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 applicatieserver 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 beschikbaar 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 ServiceWorker ze weergeeft, hangt af van wat je erin gedefinieerd hebt – in het meest simpele geval verschijnt de payloadstring als tekstbody in het bericht.
Het back-endscript – dat je natuurlijk moet beveiligen tegen onbevoegde toegang met bijvoorbeeld .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 versleutelde vracht verstuurt via HTTPS-POST. Op alle apparaten en browsers waar je een pushabonnement mee hebt afgesloten, moet dan een bericht verschijnen.
Nieuwe berichten
Een website heeft met pushberichten een krachtige tool die de technische kloof met mobiele apps duidelijk verkleint. Er kleeft in vergelijking 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 besturingssystemen zich bekommeren om de berichten waarop browsers geabonneerd zijn.
Op zit moment zijn iOS, Safari en Edge bezig om hun achterstand op dit gebied in te halen, zodat het aantal ontbrekende platforms steeds kleiner wordt. Nu is dan ook een mooi moment om je eens goed met pushberichten bezig te houden. Ook op websites kom je de vraag naar toestemming hiervoor steeds vaker tegen.
De technische drempels liggen daarbij wel hoog, maar de mogelijkheid om bezoekers rechtstreeks te kunnen bereiken is die inzet wel waard. Maar gebruik dit kanaal wel met enige voorzichtigheid: gebruikers kunnen de pushberichten immers met twee of drie muisklikken weer tot zwijgen brengen – waarna je een tweede kans niet meer zult krijgen. (nkr)
literatuur
[1] Herbert Braun, Push-Web, Pushberichten in de
browser ontvangen – deel 1, c't 6/2018, p.134