Aller au contenu principal

Mise en place d'un système de messages instantanés avec Mercure

Table des Matières
  1. Introduction à Mercure
  2. Architecture du système
  3. Installation et configuration de Mercure
  4. Configuration Apache et SSL
  5. Génération des clés JWT
  6. Exemple d'implémentation backend
  7. Exemple d'implémentation frontend

1. Introduction à Mercure

Mercure est un protocole de communication temps réel basé sur les Server-Sent Events (SSE). Contrairement à WebSockets, Mercure utilise le protocole HTTP standard, ce qui le rend plus simple à implémenter et compatible avec le cache HTTP.

Avantages de Mercure

  • Simple : Utilise HTTP/1.1 et HTTP/2
  • Économe : Moins de ressources que WebSockets
  • Compatibilité : Fonctionne derrière les proxies
  • Reconnexion automatique : Gère les déconnexions
  • Support natif dans la majorité des navigateurs

2. Architecture du système

Architecture des messages instantanés via mercure

  1. Le client (navigateur, app mobile, etc.) ouvre une connexion HTTP/HTTPS vers le hub Mercure (via le domaine géré par le vhost Apache, ex : messages.habeuk.com).
  2. Le vhost Apache joue uniquement le rôle de proxy : il redirige la requête vers le conteneur Mercure (souvent sur 127.0.0.1:3000).
  3. L’application Symfony publie un message sur Mercure (via le HubInterface et un Update).
  4. Le hub Mercure envoie ensuite ce message en temps réel à tous les clients abonnés au topic concerné (via la connexion SSE ouverte au début).

3. Installation et configuration de Mercure

3.1. Configuration Docker Compose

Créer un dossier ou vous souhaitez gérer le serveur mercure, (exemple 'mercure_habeuk'), accéder à ce dossier et créer le fichier de configuration de mercure (docker-compose.yml).

# compose.yaml
services:
  mercure:
    image: dunglas/mercure
    restart: unless-stopped
    container_name: mercure_habeuk
    environment:
      # Au niveau de mercure on ecoute sur le port 80.( mercure est dans un docker ).
      SERVER_NAME: ':80'
      MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_PUBLISHER_JWT_KEY}"
      MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_SUBSCRIBER_JWT_KEY}" 
      MERCURE_EXTRA_DIRECTIVES: |
        cors_origins https://www.habeuk.com https://habeuk.com     
        anonymous    
    ports:
      - "127.0.0.1:3000:80"   
    # configuration avec volumes nommés gerer par docker ( /var/lib/docker/volumes/... )
    volumes:
      - mercure_data:/data
      - mercure_config:/config
volumes:
  mercure_data:
  mercure_config:

Explications :

SERVER_NAME: ':80' → le chiffre 80 indique le port sur lequel le serveur Mercure écoutera à l’intérieur du conteneur Docker. Comme Mercure tourne dans un conteneur isolé, ce port ne rentre pas en conflit avec celui de la machine hôte. On aurait d’ailleurs pu choisir un autre port interne si nécessaire.

MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_PUBLISHER_JWT_KEY}" et MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_SUBSCRIBER_JWT_KEY}" → ce sont les clés JWT utilisées respectivement pour publier et souscrire aux événements Mercure. Elles sont définies dans le fichier d’environnement mercure_habeuk/.env

MERCURE_EXTRA_DIRECTIVES → permet d’ajouter des instructions supplémentaires à la configuration du hub Mercure (c’est comme un petit bloc de configuration interne).

cors_origins https://www.habeuk.com https://habeuk.com → définit les origines autorisées (CORS) qui peuvent se connecter au hub Mercure. Cela signifie que seules les pages web servies depuis https://www.habeuk.com et https://habeuk.com auront le droit : 
- de s’abonner aux événements (via EventSource) ;
- et d’envoyer des requêtes de publication (si les droits JWT le permettent).

anonymous → autorise les connexions sans jeton JWT pour la souscription. En d’autres termes, les clients peuvent s’abonner à certains topics sans avoir besoin de s’authentifier, ce qui est utile pour les flux publics (comme des messages visibles par tous ou des notifications globales).

ports:
127.0.0.1:3000:80 → indique à Docker de rediriger le port 80 du conteneur Mercure vers le port 3000 de la machine hôte, mais uniquement accessible depuis 127.0.0.1 (localhost).

En d’autres termes : - Le service Mercure écoute sur le port 80 à l’intérieur du conteneur. - Ce port est exposé localement sur le port 3000 de la machine hôte. - L’adresse 127.0.0.1 limite l’accès à la machine locale uniquement (personne ne peut atteindre le port 3000 depuis l’extérieur).

3.2. Démarrage du service

installation et démarrage de mercure

docker compose up -d

arrêter et redémarrer le service

docker compose down && docker compose up -d

3.2. Test du service Mercure

On vérifie en premier si le serveur mercure fonctionne sur l'@ ip et le port définit. (directement sur le VPS)

curl http://127.0.0.1:3000/.well-known/mercure?topic=test

Si tout est correct, la commande doit ouvrir une connexion SSE.

Nous souhaitons que le serveur mercure puisse être accessible via ce sous domaine messages.habeuk.com.

On modifier le fichier /etc/hosts en y ajoutant ce qui suit:

127.0.0.1	messages.habeuk.com

Puis on effectue un autre test 

curl http://messages.habeuk.com:3000/.well-known/mercure?topic=test

Si tout est correct, la commande doit ouvrir une connexion SSE.

4. Configuration Apache et SSL

Nous souhaitons une adresse propre, c'est-à-dire messages.habeuk.com sans le numéro de port. Apache va intervenir afin de transférer les requêtes reçues sur messages.habeuk.com vers l'adresse 127.0.0.1:3000 de manière transparente pour l'utilisateur.

Nous devons ajouter un virtual host dans /etc/apache2/sites-available/messages.habeuk.com.conf :

<VirtualHost *:80>
    ServerName messages.habeuk.com
    # Proxy toutes les requêtes HTTP vers Mercure (port 3000)
    ProxyPreserveHost On
    ProxyRequests Off
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/
    #
    ErrorLog /var/www/habeuk/logs/error-mercure.log
    CustomLog /var/www/habeuk/logs/access-mercure.log combined
</VirtualHost>

Activation des modules Apache 2 :

sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod headers
sudo a2enmod rewrite

Activation du virtual host :

sudo a2ensite messages.habeuk.com.conf

Redémarrage d'Apache 2 :

sudo systemctl restart apache2

Ensuite, on effectue un test :

curl http://messages.habeuk.com/.well-known/mercure?topic=test

Si tout est correct, la commande doit ouvrir une connexion SSE.

Vous pouvez aussi vérifier directement dans votre navigateur : http://messages.habeuk.com doit afficher la page d'accueil de Mercure.

Activez le SSL sur votre sous-domaine messages.habeuk.com en suivant cette documentation.

Mettez à jour le fichier virtual host /etc/apache2/sites-available/messages.habeuk.com.conf :

<VirtualHost *:80>
    ServerName messages.habeuk.com
    Redirect permanent / https://messages.habeuk.com/
</VirtualHost>

<VirtualHost *:443>
    ServerName messages.habeuk.com
    # Proxy toutes les requêtes HTTPS vers Mercure (port 3000)
    ProxyPreserveHost On
    ProxyRequests Off
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/
    #
    ErrorLog /var/www/habeuk/logs/error-mercure.log
    CustomLog /var/www/habeuk/logs/access-mercure.log combined
    # Activation SSL
    SSLEngine On
    SSLCertificateFile /etc/letsencrypt/live/habeuk.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/habeuk.com/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>

Enfin, refaites le test précédent, cette fois avec HTTPS :
https://messages.habeuk.com

5. Génération des clés JWT

La sécurité de Mercure repose sur des JWT (JSON Web Tokens) signés avec des clés cryptographiques. Générez des secrets aléatoires suffisamment longs à l'aide de la commande suivante  :

openssl rand -base64 32

Créez ensuite le fichier mercure_habeuk/.env et ajoutez-y le contenu suivant :

MERCURE_PUBLISHER_JWT_KEY=your-secure-publisher-key-from-openssl 
MERCURE_SUBSCRIBER_JWT_KEY=your-secure-subscriber-key-from-openssl 

6. Exemple d’implémentation backend (via symfony)

<?php

// src/Service/ChatService.php
namespace App\Service;

use App\Entity\ {
  Notification,
  NotificationReceipt
};
use App\Enum\NotificationType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;

class SendNotificationByMercureService {

  public function __construct(private EntityManagerInterface $em, private HubInterface $hub, private SerializerInterface $serializer, private LoggerInterface $logger) {
  }

  public function publishViaMercure(Notification $notification): void {
    $receipts = $notification->getReceipts();
    $topics = [];
    if ($receipts->count() > 0) {
      foreach ($receipts->getValues() as $receipt) {
        /**
         *
         * @var NotificationReceipt $receipt
         */
        $topics[] = 'notification/' . $receipt->getRecipient()->getId();
      }
      try {
        $result = $this->serializer->serialize($notification, 'json',
          [
            AbstractNormalizer::GROUPS => [
              'notification:read'
            ],
            AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object) {
              return $object->getId();
            }
          ]);
        $type = $notification->getType() == NotificationType::NEW_MESSAGE ? "new_message" : "notification";
        $update = new Update($topics, $result, true, null, $type);
        $this->hub->publish($update);
      }
      catch (\Exception $e) {
        $this->logger->error($e->getMessage());
      }
    }
  }
}

7. Exemple d’implémentation frontend

// mercureClient.js
class MercureClient {
    constructor(url) {
        this.eventSource = new EventSource(url, { withCredentials: true });
        this.eventSource.onerror = (err) => {
            console.error("Mercure connection error:", err);
        };
    }

    build() {
        const events = ["chat", "read_message", "notification", "new_message"];
        events.forEach((event_name) => {
            this.Listerner(event_name);
        });
        this.eventSource.onmessage = (event) => {
            console.log("MercureClient event onmessage : ", event);
            alert("ezsd");
        };
    }

    Listerner(event_name) {
        this.eventSource.addEventListener(event_name, (event) => {
            const data = JSON.parse(event.data);
            this.dispatchCustomEvent(event_name, data);
        });
    }

    dispatchCustomEvent(event_name, data) {
        console.log("dispatchCustomEvent event_name : ", event_name, data);
        document.dispatchEvent(
            new CustomEvent(`mercure-${event_name}`, {
                detail: data,
            })
        );
    }
}
export default MercureClient;
Profile picture for user admin Stephane K

Écrit le

Il y'a 2 semaines
Modifié
Il y'a 2 semaines
Loading ...
WhatsApp
Habeuk Support: +49 152 108 01753
Envoyer