Commit 7383ab06 authored by Kourser's avatar Kourser
Browse files

Un dépôt fraîchement cloné doit pouvoir passer un appel

Deux frictions rencontrées en éprouvant le lot précédent, et qui coûtaient
chacune une demi-heure à qui les découvre.

LIVEKIT_INTERNAL_URL valait « http://livekit:7880 » dans l'exemple, une
adresse qui ne résout que dans la composition. Le commentaire disait
pourtant « laissez vide hors conteneur » — consigne juste et inapplicable,
puisque vide on retombait sur l'adresse du navigateur, en « ws:// », que le
SDK d'administration ne sait pas composer. Lancer l'API sur sa machine
donnait donc « ENOTFOUND livekit » si on suivait l'exemple, et « fetch
failed » si on suivait le commentaire.

L'adaptateur traduit désormais le schéma au lieu de le recopier : ws
devient http, wss devient https, c'est le même port. L'exemple laisse la
variable vide, et elle n'a plus de raison d'être renseignée que pour un
serveur média mutualisé.

Le fichier livekit livré annonçait l'adresse de pont du conteneur. Aucun
navigateur ne la joint — pas même celui de la machine hôte, sur Docker
Desktop. Le fichier prétendait servir « la machine d'un développeur » et ne
la servait pas : l'appel se montait, tenait quinze secondes, et tombait sur
« ICE failed ». Il annonce maintenant la boucle locale, ce qui est vrai
pour un poste de développement et faux assez bruyamment ailleurs.

Ce changement en imposait un autre, sans quoi il aggravait les choses.
L'installateur n'écrivait la configuration de la machine que sous la voie
« cette machine porte déjà un LiveKit, on déplace les ports » : prendre la
voie ordinaire sur un vrai serveur y aurait laissé la boucle locale. Il
l'écrit désormais dès que l'adresse publique n'est pas locale, quels que
soient les ports — ce qui referme au passage le défaut vécu la semaine
dernière, où la voie ordinaire laissait « use_external_ip: false » sur un
serveur joignable.

Éprouvé des deux côtés : le générateur laisse le fichier livré en place
pour « localhost » et écrit l'autre pour un vrai domaine ; et un appel
local tient au-delà de trente secondes avec le dépôt tel qu'il sort, sans
une variable renseignée. Il tombait à quinze.

976 garanties, dont trois neuves : l'adresse déduite, l'exemple qui reste
vide, et le fichier livré qui n'annonce que la boucle locale — celle-là
tombe si quelqu'un y met une adresse de serveur.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent f0219a66
Loading
Loading
Loading
Loading
Loading
+14 −8
Changes for .env.example: 14 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -66,14 +66,20 @@ S3_SECRET=CHANGEZ-MOI-secret-stockage
# EN   from the cell's workstations. An empty LIVEKIT_URL disables video
# EN   conferencing; the product says so.
LIVEKIT_URL=ws://localhost:7880
# Adresse de l'API média vue depuis le serveur. Dans la composition Docker,
# l'hôte est le nom du service : « localhost » y désigne le conteneur
# applicatif lui-même. Laissez vide hors conteneur.
#
# EN — Address of the media API as seen from the server. In the Docker
# EN   composition the host is the service name: "localhost" there means the
# EN   application container itself. Leave empty outside a container.
LIVEKIT_INTERNAL_URL=http://livekit:7880
# Adresse de l'API média vue depuis le serveur, à renseigner UNIQUEMENT pour un
# serveur média mutualisé. La composition donne la sienne au conteneur, et hors
# conteneur l'adresse se déduit de LIVEKIT_URL — « ws:// » devient « http:// »,
# c'est le même port.
#
# La valeur « http://livekit:7880 » figurait ici par défaut : elle est juste
# dans la composition et introuvable hors d'elle, si bien que lancer l'API sur
# sa machine échouait sur « ENOTFOUND livekit » au premier appel.
#
# EN — Address of the media API as seen from the server, to be filled in ONLY
# EN   for a shared media server. The composition gives its own to the
# EN   container, and outside a container the address is derived from
# EN   LIVEKIT_URL — "ws://" becomes "http://", same port.
LIVEKIT_INTERNAL_URL=
LIVEKIT_KEY=kastell
LIVEKIT_SECRET=CHANGEZ-MOI-secret-media-au-moins-32-caracteres

+22 −1
Changes for apps/api/src/adaptateurs/media.ts: 22 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -88,8 +88,29 @@ export interface ConfigurationMedia {
  };
}

/**
 * L'adresse d'administration, quand personne ne l'a dite.
 *
 * `.env.example` invite à laisser LIVEKIT_INTERNAL_URL vide hors conteneur, et
 * la composition fournit la sienne au conteneur — la variable n'a donc de
 * raison d'être renseignée que sur un serveur média mutualisé.
 *
 * Mais vide, on retombait sur l'adresse du navigateur : « ws://… », que le SDK
 * d'administration ne sait pas composer — il attend http(s). Qui lance l'API
 * sur sa machine récoltait « fetch failed » au premier appel, sans que rien ne
 * désigne la bonne variable. La consigne de la documentation était donc juste
 * et inapplicable.
 *
 * On traduit le schéma plutôt que de le recopier. C'est la même adresse : la
 * signalisation et l'API d'administration vivent sur le même port.
 */
export function adresseAdministration(c: ConfigurationMedia): string {
  if (c.urlInterne) return c.urlInterne;
  return c.url.replace(/^ws(s?):\/\//i, "http$1://");
}

export function mediaLiveKit(c: ConfigurationMedia): Media {
  const administration = c.urlInterne ?? c.url;
  const administration = adresseAdministration(c);
  const salles = new RoomServiceClient(administration, c.cle, c.secret);
  const egress = new EgressClient(administration, c.cle, c.secret);

+26 −0
Changes for apps/api/src/verification/composition.ts: 26 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -90,6 +90,18 @@ export async function verifierComposition(r: Rapport): Promise<void> {
    laisseesVides.length > 0 && motif === "", motif);

  await verifierPortMedia(compose, r);

  // ── Ce que l'exemple promet, il doit le tenir ────────────────────────────
  //
  // « Laissez vide hors conteneur » était juste, et l'exemple livrait quand
  // même « http://livekit:7880 » — une adresse qui ne résout que dans la
  // composition. Qui lançait l'API sur sa machine récoltait « ENOTFOUND
  // livekit » au premier appel média, avec pour seule piste une variable que
  // la documentation lui disait de laisser vide.
  const exemple = await readFile(`${racine}.env.example`, "utf8");
  r.verifie("EX-26", "l'exemple laisse l'adresse média interne vide",
    /^LIVEKIT_INTERNAL_URL=\s*$/m.test(exemple),
    /^LIVEKIT_INTERNAL_URL=(.*)$/m.exec(exemple)?.[1] ?? "absente");
}

/**
@@ -131,4 +143,18 @@ async function verifierPortMedia(compose: string, r: Rapport): Promise<void> {
  r.verifie("EF-803", "et c'est le même numéro des deux côtés",
    declare !== null && publie !== null && declare[1] === publie[1],
    `livekit.yaml ${declare?.[1] ?? "—"} / compose ${publie?.[1] ?? "—"}`);

  // ── Le fichier livré est celui d'un poste de développement ───────────────
  //
  // Il annonce la boucle locale, et c'est délibéré : le navigateur et le
  // conteneur sont sur la même machine. Déployé tel quel il échoue tout de
  // suite et franchement, là où l'ancien « use_external_ip: false » annonçait
  // l'adresse de pont du conteneur — injoignable, mais silencieusement.
  //
  // L'installateur écrit l'autre dès que l'adresse publique n'est pas locale.
  // Ce contrôle tombe si quelqu'un met ici une adresse de serveur, ce qui
  // ferait du fichier livré une configuration de production par accident.
  const annonce = /^\s*node_ip:\s*(\S+)\s*$/m.exec(livekit);
  r.verifie("EF-803", "le fichier livré n'annonce que la boucle locale",
    annonce?.[1] === "127.0.0.1", annonce?.[1] ?? "aucun node_ip");
}
+22 −1
Changes for apps/api/src/verification/media.ts: 22 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -8,7 +8,9 @@ import * as reunions from "../domaine/reunions.js";
import * as chat from "../domaine/chat.js";
import { mainCourante } from "../noyau/rejeu.js";
import { verifierEnBase } from "../noyau/integrite.js";
import { definirMedia, mediaEssai, mediaLiveKit } from "../adaptateurs/media.js";
import {
  adresseAdministration, definirMedia, mediaEssai, mediaLiveKit,
} from "../adaptateurs/media.js";
import { stockage, stockageConfigure } from "../adaptateurs/stockage.js";
import type { Rapport } from "./rapport.js";

@@ -271,6 +273,25 @@ export async function verifierMedia(
    JSON.stringify(corps.controles).includes("443"));

  // ── Le jeton du fournisseur ──────────────────────────────────────────────
  // ── L'adresse d'administration, quand personne ne la dit ─────────────────
  r.titre("Adresse du serveur média");
  r.verifie("EF-801", "vide, elle se déduit de celle du navigateur",
    adresseAdministration({
      url: "ws://localhost:7880", cle: "c", secret: "s",
      stockage: { region: "r", seau: "s", acces: "a", secret: "x" },
    }) === "http://localhost:7880");
  r.verifie("EF-801", "le chiffrement suit : wss donne https",
    adresseAdministration({
      url: "wss://media.exemple.fr", cle: "c", secret: "s",
      stockage: { region: "r", seau: "s", acces: "a", secret: "x" },
    }) === "https://media.exemple.fr");
  r.verifie("EF-801", "renseignée, elle l'emporte — le cas du serveur mutualisé",
    adresseAdministration({
      url: "wss://media.exemple.fr", urlInterne: "http://172.17.0.1:7880",
      cle: "c", secret: "s",
      stockage: { region: "r", seau: "s", acces: "a", secret: "x" },
    }) === "http://172.17.0.1:7880");

  r.titre("Jeton média");
  const vrai = mediaLiveKit({
    url: "ws://livekit.invalide", cle: "cle-de-verification",
+12 −1
Changes for docker/livekit.yaml: 12 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -22,7 +22,18 @@ rtc:
  # que la signalisation ou le TURN — c'est-à-dire de la façon qui marche déjà
  # partout. Il épargne aussi cent processus « docker-proxy » sur l'hôte.
  udp_port: 50000
  use_external_ip: false
  # Adresse annoncée aux navigateurs. La boucle locale, parce que ce fichier
  # est celui d'un poste de développement : le navigateur et le conteneur y
  # sont sur la même machine, et Docker publie ce port sur elle.
  #
  # Ne le déployez pas tel quel. Un conteneur qui annonce sa propre adresse de
  # pont — ce que fait « use_external_ip: false » — n'est joignable par aucun
  # navigateur extérieur, et l'appel échoue sur « ICE failed » sans rien dire
  # d'utile. La boucle locale échoue au moins franchement, et tout de suite.
  #
  # L'installateur écrit docker/livekit.local.yaml dès que l'adresse publique
  # n'est pas locale, avec « use_external_ip: true » et le vrai domaine TURN.
  node_ip: 127.0.0.1

# Relais TURN.
#
Loading