Mercure, un protocole pour pousser des mises à jour vers des navigateurs et app mobiles en temps réel

Retrouvez la version EN de cet article ici.

Mercure est un protocole permettant de transmettre en temps réel des mises à jour de données vers les navigateurs web (ou autres clients HTTP) de manière fiable, rapide et économe en énergie. Mercure est particulièrement utile pour publier des mises à jour en temps réel des ressources exposées via des API Web (ex : pour être consommées par des applications web et mobiles réactives). Le protocole a été publié en tant qu’Internet Draft, et est maintenu dans ce dépôt GitHub.

Une implémentation de référence du hub Mercure (le serveur) est également disponible sur ce dépôt. Cette implémentation écrite en Go est publiée sous licence libre (AGPL). Une bibliothèque Go (qui peut être utilisée dans n'importe quelle application écrite en ce langage, qui n’a alors plus besoin de Hub) ainsi qu’une image officielle Docker sont également fournies.

Jetez un oeil à la démo !

En complément à la version Open Source, une version haute disponibilité (hébergée ou on premise) de Mercure est disponible en bêta privée.

Mercure en quelques mots 

  • Support natif de navigateurs, pas de bibliothèque ni de SDK requis (basé sur les “Server-Sent Events”).
  • Compatible avec tous les serveurs existants, même ceux qui ne supportent pas les connexions persistantes (architecture serverless, PHP, FastCGI…).
  • Support natif de la reconnexion et de la récupération des événements perdus.
  • Mécanisme d'autorisation basé sur JWT (envoi sécurisé d'une mise à jour à certains “abonnés” sélectionnés).
  • Exploite le multiplexage HTTP/2.
  • Conçu avec pour les API hypermédia, supporte également GraphQL.
  • Auto-découvrable grâce au web linking.
  • Supporte le chiffrement.
  • Fonctionne même avec les vieux navigateurs en utilisant un polyfill (IE7+).
  • Support du push sans connexion dans des environnements contrôlés (délégation aux protocoles spécifiques des opérateurs mobiles). 

Implémentation de référence du hub

  • Rapide, écrit en Go
  • Fonctionne partout : binaires statiques et images Docker disponibles
  • Support automatique des protocoles HTTP/2 et HTTPS (via Let's Encrypt)
  • Cloud Natif, suit la méthodologie Twelve-Factor App
  • Open source (Affero General Public License) 

Exemples

Exemple d’un client (subscriber), en JavaScript :  


// The subscriber subscribes to updates for the https://example.com/foo topic
// and to any topic matching https://example.com/books/{name}
const url = new URL('https://hub.example.com/subscribe');
url.searchParams.append('topic', 'https://example.com/books/{id}');
url.searchParams.append('topic', 'https://example.com/users/dunglas');

const eventSource = new EventSource(url);

// The callback will be called every time an update is published
eventSource.onmessage = e => console.log(e); // do something with the payload

L'URL du hub peut être découverte (optionnel) :


fetch('https://example.com/books/1') // Has this header `Link: ; rel="mercure"`
    .then(response => {
        // Extract the hub URL from the Link header
        const hubUrl = response.headers.get('Link').match(/<(.*)>.*rel="mercure".*/)[1];
        // Subscribe to updates using the first snippet, do something with response's body...
    });

Pour publier une mise à jour, un serveur d'application ou un navigateur web (publisher) a juste à envoyer une requête HTTP POST vers le hub. Voici un exemple utilisant Node.js / Serverless :

// Handle a POST, PUT, PATCH or DELETE request or finish an async job...
// and notify the hub
const https = require('https');
const querystring = require('querystring');

const postData = querystring.stringify({
    'topic': 'https://example.com/books/1',
    'data': JSON.stringify({ foo: 'updated value' }),
});

const req = https.request({
    hostname: 'hub.example.com',
    port: '443',
    path: '/publish',
    method: 'POST',
    headers: {
        Authorization: 'Bearer ', // the JWT key must be shared between the hub and the server
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(postData),
    }
}, /* optional response handler */);
req.write(postData);
req.end();

// You'll probably prefer use the request library or the node-fetch polyfill in real projects,
// but any HTTP client, written in any language, will be just fine

Des exemples dans d’autres langages sont disponibles dans le répertoire examples du dépôt.

Cas d’usages

Disponibilité en direct

  • Une Progressive Web App récupère l'état de disponibilité d'un produit à partir d'une API REST et l'affiche : un seul est toujours disponible.
  • 3 minutes plus tard, le dernier produit est acheté par un autre client.
  • La PWA montre instantanément que le produit n’est plus disponible

Emplois asynchrones

  • Une PWA dit au serveur de calculer un rapport. Cette tâche est coûteuse et mettra du temps à se terminer.
  • Le serveur délègue le calcul du rapport à un opérateur asynchrone (à l’aide des message queues) et ferme la connexion avec la PWA.
  • Le worker envoie le rapport à la PWA lorsque le rapport est calculé

Montage collaboratif

  • Une webapp permet à plusieurs utilisateurs de modifier le même document simultanément.
  • Les modifications apportées sont immédiatement diffusées à tous les utilisateurs connectés. 

Mercure couvre tout ça !

Spécification du protocole

La spécification complète du protocole se trouve dans spec/mercure.md. Elle est également disponible en tant qu’Internet Draft et est conçue pour être publiée en tant que RFC.

Implémentation du hub

Usage

Binaire pré-compilé

Téléchargez un binaire à partir de notre page GitHub exécutez ceci :

JWT_KEY=myJWTKey ADDR=:3000 DEMO=1 ALLOW_ANONYMOUS=1 PUBLISH_ALLOWED_ORIGINS=http://localhost:3000 ./mercure

Le serveur est désormais disponible sur http://localhost:3000 avec le mode démo activé. ALLOW_ANONYMOUS étant défini sur 1, les abonnés anonymes sont autorisés. Pour l’exécuter en mode production, et générer automatiquement un certificat Let's Encrypt, lancez la commande suivante en root :

PUBLISHER_JWT_KEY=myPublisherKey SUBSCRIBER_JWT_KEY=mySubcriberKey ACME_HOSTS=example.com ./mercure

La valeur de la variable d'environnement ACME_HOSTS doit être mise à jour pour correspondre à votre/vos nom(s) de domaine. Un certificat Let's Encrypt sera généré automatiquement. Si vous omettez cette variable, le serveur sera exposé sur une connexion HTTP (non sécurisée).

Lorsque le serveur est opérationnel, les endpoints suivants sont disponibles :

  • POST https://example.com/hub: pour publier des mises à jour.
  • GET https://example.com/hub: pour s’abonner à des mises à jour.

Consultez le protocole pour connaître tous les détails. Pour compiler la version de développement et activer la page de démonstration, consultez le fichier CONTRIBUTING.md.

Image Docker 

Une image Docker est disponible sur le Docker Hub. La commande suivante est tout ce qu’il vous faudra pour obtenir un serveur fonctionnel, mode démo activé :


docker run \
    -e PUBLISHER_JWT_KEY=myPublisherKey -e SUBSCRIBER_JWT_KEY=mySubcriberKey -e DEMO=1 -e ALLOW_ANONYMOUS=1 \
    -p 80:80 \
    dunglas/mercure

Le serveur, en mode démo, est disponible à : http://localhost. Les “abonnés” anonymes sont autorisés.

En production, exécutez ceci :


docker run \
    -e JWT_KEY=myKey -e ACME_HOSTS=example.com \
    -p 80:80 -p 443:443 \
    dunglas/mercure

Assurez-vous de mettre à jour la valeur de ACME_HOSTS pour qu’elle corresponde à votre/vos nom(s) de domaine.

Variables d'environnement

  • ACME_CERT_DIR : le répertoire où stocker les certificats Let's Encrypt
  • ACME_HOSTS : une liste d'hôtes séparés par des virgules pour lesquels les certificats Let's Encrypt doivent être générés
  • ADDR : l’adresse à utiliser (exemple : 127.0.0.1:3000, valeur par défaut :80 ou :http ou :https en fonction de si HTTPS est activé ou non)
  • ALLOW_ANONYMOUS : mis à 1 pour permettre aux abonnés sans JWT de se connecter
  • DB_PATH : le chemin vers la base de données bbolt (par défaut : updates.db, dans le répertoire courant)
  • CERT_FILE : un fichier de certificat (pour utiliser un certificat personnalisé)
  • CERT_KEY : une clé de certificat (pour utiliser un certificat personnalisé)
  • CORS_ALLOWED_ORIGINS : une liste d’hôtes CORS autorisés, séparés par des virgules
  • PUBLISH_ALLOWED_ORIGINS : une liste d’hôtes autorisés à publier des mises à jours (nécessaire uniquement en mode autorisation par cookie, afin de prévenir les attaques de type CSRF)
  • DEBUG : définir à 1 pour activer le mode débogage
  • DEMO : définir à 1 pour activer le mode démo (activé automatiquement lorsque DEBUG=1)
  • LOG_FORMAT : le format des logs à utiliser : JSON, FLUENTD ou TEXT (par défaut)
  • PUBLISHER_JWT_KEY : doit contenir la clef secrète pour valider le JWT des publishers
  • SUBSCRIBER_JWT_KEY : doit contenir la clef secrète pour valider le JWT des subscribers
  • JWT_KEY : raccourci pour régler les variables SUBSCRIBER_JWT_KEY et PUBLISHER_JWT_KEY à la même valeur

Si ACME_HOSTS ou CERT_FILE et CERT_KEY sont fournis, un serveur HTTPS supportant une connexion HTTP/2 sera démarrée. Si ce n’est pas le cas, un serveur HTTP sera démarré (non sécurisé). 

FAQ

Comment utiliser Mercure avec GraphQL ?

Parce qu’ils sont conçus pour être indépendants du transport utilisé, Mercure est parfaitement adapté aux abonnements GraphQL (subscriptions). En réponse à la requête d'abonnement, le serveur GraphQL peut renvoyer l’URL du sujet Mercure (topic) correspondante. Le client peut ensuite s'abonner au flux d'événements Mercure correspondant à cet abonnement en créant un nouveau EventSource en lui passant en paramètre une URL comme celle-ci : https://example.com/hub?topic=https://example.com/subscriptions/<subscription-id>

Les mises à jour relatives à cet abonnement peuvent ensuite être envoyées du serveur GraphQL aux clients, via le hub Mercure (dans la propriété data de l'événement envoyé par le serveur).

Pour se désabonner, le client a juste à appeler EventSource.close()

Mercure peut être facilement intégré au framework Apollo GraphQL en créant un transport dédié.

Quelle est la différence entre Mercure et Websocket ?  

Websocket est un protocole de bas niveau, tandis que Mercure est un protocole de haut niveau. Mercure fournit des fonctionnalités intégrées très pratiques telles que l’autorisation, la reconnexion automatique ou la réconciliation des états, tandis qu'avec WebSocket, vous devez les implémenter vous-même. Contrairement à Mercure (qui est construit sur des server-Sent Events), Websocket n’est pas conçu pour tirer parti de HTTP/2.

Les connexions HTTP/2 étant multiplexées et bidirectionnelles par défaut (ce qui n'était pas le cas de HTTP/1), lorsque vous utilisez Mercure via une connexion h2 (recommandé), votre application peut à la fois recevoir des données (via des Server-Sent Events) et envoyer des données au serveur (avec des requêtes POST) en ré-utilisant la même connexion, donc sans surcoût.

Dans la plupart des cas, Mercure peut être considéré comme solution de remplacement de WebSocket, moderne et plus simple à utiliser.

Quelle est la différence entre Mercure et WebSub ?

WebSub est un protocole serveur à serveur tandis que Mercure est un protocole qui supporte également les connexions serveur à client et client à client. Mercure a été fortement inspiré par WebSub, et nous avons tenté de le rendre aussi proche que possible de ce protocole.

Mercure utilise les server-sent-clients pour distribuer les mises à jour, tandis que WebSub utilise les requêtes POST. De plus, Mercure dispose d'un mécanisme d'autorisation avancé et permet de souscrire à plusieurs sujets avec une seule connexion utilisant des URL templates.

Quelle est la différence entre Mercure et Web Push ?

L'API Push est principalement conçue pour envoyer des notifications aux appareils mobiles non connectés à l'application. Dans la plupart des implémentations de WebPush, la taille des messages est très limitée. De plus, les messages sont envoyés via les API et les serveurs propriétaires des sociétés créant les navigateurs et les systèmes d'exploitation mobiles.

Mercure, quant à lui, est conçu pour envoyer des mises à jour en direct aux périphériques actuellement connectés via un site web ou une app mobile. La taille des messages n’est pas limitée, et le message est directement transmis de vos serveurs vers les clients. En bref, utilisez l'API Push pour envoyer des notifications aux utilisateurs hors connexion (qui seront affichées dans les centres de notification de Chrome, d’Android ou iOS) et utilisez Mercure pour recevoir des mises à jour en direct à l’application lorsque l'utilisateur l’utilise.

 

Vous aimez Mercure ? Testez-le, donnez lui une étoile sur GitHub, et n’hésitez pas à y contribuer ! De notre côté, nous avons proposé un client expérimental pour PHP au projet Symfony. Restez connectés pour en savoir plus !