Mise en œuvre d'un serveur Coturn et développement d'un service de signalement pour WebRTC

Installation du serveur Coturn

Pour commencer, nous devons récupérer les sources de Coturn et compiler le binaire sur un environnement Linux (Ubuntu/CentOS).

git clone https://github.com/coturn/coturn
cd coturn
./configure
make
sudo make install

Vérifiez que l'installatoin est opératinonelle en exécutant la commande suivante pour localiser le binaire :

which turnserver

Configuration des identifiants et du service

Générez un utilisateur initial pour votre serveur TURN :

turnadmin -k -u utilisateur_test -r ma_region -p motdepasse123

Éditez ensuite le fichier de configuration principal, généralement situé dans /usr/local/etc/turnserver.conf. Voici une configuration optimisée pour un usage simplifié derrière un NAT :

# Configuration du port de réception
listening-port=3478
# Adresse IP interne du serveur
listening-ip=172.17.67.146
# Port sécurisé TLS
tls-listening-port=5349
# Adresse IP publique (External IP)
external-ip=203.0.113.45

# Mécanisme d'authentification
lt-cred-mech
user=utilisateur_test:motdepasse123
relay-ip=0.0.0.0/0

# Plage de ports pour le relais
min-port=49152
max-port=49535

# Gestion des logs
verbose
log-file=/var/log/turnserver.log

Génération de certificats SSL auto-signés

Si vous n'avez pas de certificat valide, générez une paire de clés pour sécuriser les échanges TLS :

sudo openssl req -x509 -newkey rsa:2048 -keyout /etc/turn_private.pem -out /etc/turn_cert.pem -days 3650 -nodes

Exécution du service

Lancez le serveur en spécifiant l'IP publique et le mode souhaité :

turnserver -v -r 203.0.113.45:3478 -a -o

Pour arrêter le service, utilisez la commande de terminaison de processus :

pkill -9 turnserver

Client de test WebRTC (Application de Chat)

Le code suivant permet de tester la connectivité TURN entre plusieurs clients. Modifiez les constantes ICE_CONFIG et SIGNALING_URL avec vos propres paramètres.


<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Client WebRTC Multi-Utilisateurs</title>
    <style>
        body { font-family: sans-serif; padding: 20px; background: #f4f4f9; }
        .container { max-width: 700px; margin: auto; background: white; padding: 20px; border-radius: 8px; }
        #log-area { width: 100%; height: 250px; margin-top: 10px; font-family: monospace; font-size: 12px; }
        .controls { display: flex; gap: 10px; margin-bottom: 15px; }
        input { padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
        button { cursor: pointer; padding: 8px 15px; background: #007bff; color: white; border: none; border-radius: 4px; }
    </style>
</head>
<body>
    <div class="container">
        <div class="controls">
            <input type="text" id="room-id" placeholder="ID du salon">
            <button id="btn-join">Rejoindre</button>
            <button id="btn-clear" style="background:#dc3545">Réinitialiser</button>
        </div>
        <div id="connection-status">En attente...</div>
        <textarea id="log-area" readonly></textarea>
        <div class="controls" style="margin-top:15px">
            <input type="text" id="message-input" style="flex:1" placeholder="Entrez un message..." disabled>
            <button id="btn-send" disabled>Envoyer</button>
        </div>
    </div>

    <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
    <script>
        const ICE_CONFIG = {
            iceServers: [{ urls: "turn:203.0.113.45:3478", username: "utilisateur_test", credential: "motdepasse123" }]
        };
        const SIGNALING_URL = "http://203.0.113.45:3001";

        let socket, localId, currentRoom;
        const peerConnections = new Map();
        const dataChannels = new Map();

        const log = (msg) => {
            const area = document.getElementById('log-area');
            area.value += `[${new Date().toLocaleTimeString()}] ${msg}\n`;
            area.scrollTop = area.scrollHeight;
        };

        document.getElementById('btn-join').onclick = () => {
            currentRoom = document.getElementById('room-id').value;
            if(!currentRoom) return alert("ID requis");
            initSignaling();
        };

        function initSignaling() {
            socket = io(SIGNALING_URL);
            socket.on('connect', () => {
                localId = socket.id;
                log(`Connecté au signalement : ${localId}`);
                socket.emit('join-room', currentRoom);
            });

            socket.on('assign-role', (info) => {
                log(`Rôle assigné : ${info.role}`);
                if (info.role === 'slave' && info.masterId) {
                    setupPeer(info.masterId, true);
                }
            });

            socket.on('new-peer', (data) => {
                log(`Nouveau pair détecté : ${data.peerId}`);
                setupPeer(data.peerId, false);
            });

            socket.on('signal', async (desc) => {
                const pc = peerConnections.get(desc.from);
                if (!pc) return;

                if (desc.data.type === 'offer') {
                    await pc.setRemoteDescription(new RTCSessionDescription(desc.data));
                    const answer = await pc.createAnswer();
                    await pc.setLocalDescription(answer);
                    socket.emit('signal', { to: desc.from, data: answer });
                } else if (desc.data.type === 'answer') {
                    await pc.setRemoteDescription(new RTCSessionDescription(desc.data));
                } else if (desc.data.candidate) {
                    await pc.addIceCandidate(new RTCIceCandidate(desc.data));
                }
            });
        }

        function setupPeer(remoteId, isInitiator) {
            const pc = new RTCPeerConnection(ICE_CONFIG);
            peerConnections.set(remoteId, pc);

            pc.onicecandidate = (e) => {
                if (e.candidate) socket.emit('signal', { to: remoteId, data: e.candidate });
            };

            if (isInitiator) {
                const dc = pc.createDataChannel("chat");
                setupChannel(remoteId, dc);
                pc.createOffer().then(offer => {
                    pc.setLocalDescription(offer);
                    socket.emit('signal', { to: remoteId, data: offer });
                });
            } else {
                pc.ondatachannel = (e) => setupChannel(remoteId, e.channel);
            }
        }

        function setupChannel(id, channel) {
            dataChannels.set(id, channel);
            channel.onopen = () => {
                log(`Canal ouvert avec ${id}`);
                document.getElementById('message-input').disabled = false;
                document.getElementById('btn-send').disabled = false;
            };
            channel.onmessage = (e) => log(`Reçu : ${e.data}`);
        }

        document.getElementById('btn-send').onclick = () => {
            const msg = document.getElementById('message-input').value;
            dataChannels.forEach(dc => dc.send(msg));
            log(`Moi : ${msg}`);
            document.getElementById('message-input').value = '';
        };
    </script>
</body>
</html>

Serveur de Signalement (Node.js)

Le serveur de signalement gère l'échange des offres SDP et des candidats ICE entre les clients.

const express = require('express');
const { Server } = require('socket.io');
const http = require('http');

const app = express();
const server = http.createServer(app);
const io = new Server(server, { cors: { origin: "*" } });

const salons = new Map();

io.on('connection', (socket) => {
    console.log(`Utilisateur connecté : ${socket.id}`);

    socket.on('join-room', (salonId) => {
        socket.join(salonId);
        
        if (!salons.has(salonId)) {
            salons.set(salonId, { maitre: socket.id, membres: [] });
            socket.emit('assign-role', { role: 'master' });
        } else {
            const data = salons.get(salonId);
            data.membres.push(socket.id);
            socket.emit('assign-role', { role: 'slave', masterId: data.maitre });
            io.to(data.maitre).emit('new-peer', { peerId: socket.id });
        }
    });

    socket.on('signal', (payload) => {
        io.to(payload.to).emit('signal', { from: socket.id, data: payload.data });
    });

    socket.on('disconnect', () => {
        console.log(`Déconnexion : ${socket.id}`);
        // Logique de nettoyage des salons omise pour la brièveté
    });
});

server.listen(3001, () => console.log('Serveur de signalement sur le port 3001'));

Étiquettes: coturn WebRTC Socket.io Node.js ice-server

Publié le 28 juillet à 08h51