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'));