Skip to content

Feat/broadcast all ptt#4

Open
leochely wants to merge 7 commits into
kainann:mainfrom
leochely:feat/broadcast-all-ptt
Open

Feat/broadcast all ptt#4
leochely wants to merge 7 commits into
kainann:mainfrom
leochely:feat/broadcast-all-ptt

Conversation

@leochely

@leochely leochely commented May 19, 2026

Copy link
Copy Markdown
Contributor

Résumé

Ajoute un rôle broadcaster distinct du rôle admin avec un modèle d'authentification par token : un joueur à qui l'admin a accordé ce rôle peut diffuser sa voix sur tous les canaux radio simultanément, peu importe le canal des destinataires. Utile pour les modérateurs et organisateurs d'événements RP.

Couvre serveur + console admin + client. Inclut un modèle d'auth par token push-via-WSS qui ferme un trou d'usurpation de nom inhérent au modèle de player token partagé (n'importe qui avec le player token pouvait se reconnecter sous le nom d'un broadcaster connu et hériter du rôle).

Changements

Protocole audio

Nouveau byte de flag :

Flag Signification
0x00 Proximité (inchangé)
0x01 Radio canal (inchangé)
0x02 Radio profil (inchangé)
0x03 Diffusion globale (nouveau)

Modèle d'auth broadcaster

  • Stockage : server/circusvoip_broadcasters.json au format {name: sha256_hex} (le clair du token n'est jamais persisté côté serveur).
  • AuthRegistry.issue() accepte des kwargs supplémentaires stockés dans le ticket. Nouvelle méthode AuthRegistry.verify_full() qui retourne l'entrée complète (le serveur audio en a besoin pour lire can_broadcast). verify() legacy conservé pour les appelants existants.
  • grant_broadcaster est async ; requiert que le joueur soit connecté. Le serveur génère un token (16 octets d'entropie → 32 hex chars), stocke son hash, et push {"type": "broadcaster_token_granted", "token": "<clair>"} directement sur la WebSocket du joueur cible. Pas d'affichage en clair côté admin, aucune transmission hors-bande.
  • revoke_broadcaster push {"type": "broadcaster_revoked"} au client si connecté, pour qu'il efface son token local.
  • Le client présente le token sauvegardé via le champ broadcaster_token du message join. Verification serveur en temps constant (secrets.compare_digest).

Serveur positions (circusvoip_server.py)

  • Trois commandes admin : grant_broadcaster {name}, revoke_broadcaster {name}, list_broadcasters.
  • Le ticket audio émis à l'auth porte can_broadcast: bool (True uniquement si le token a été vérifié).
  • Message welcome étendu :
    • server_caps: ["broadcast_all"] (capability discovery)
    • is_broadcaster: bool (état du joueur courant)
  • Message admin_welcome étendu : broadcasters: [...]. Push broadcasters_list envoyé aux admins uniquement sur grant/revoke.
  • Nouveau au join :
    • Refuse si le nom est déjà utilisé par une connexion en cours → error: name_in_use. Ferme aussi le scénario "reconnexion sous un autre nom pendant que mon ancien WS est encore actif".
    • Si le nom figure dans _broadcasters, exige un broadcaster_token valide → sinon error: broadcaster_token_invalid. Protège l'identité même pour les non-broadcasters.

Serveur audio (circusvoip_audio_server.py)

  • Au join, lit can_broadcast du ticket via verify_full() et le stocke dans state.client_caps[ws].
  • Avant relais, vérifie chaque trame binaire : si flag == 0x03 et que l'émetteur n'a pas can_broadcast, drop la trame.
  • Log rate-limité à 1× par minute par client refusé pour éviter d'inonder les logs si un client non-broadcaster tient la touche en continu (50 trames/s droppées).

Console admin (circusvoip_admin.py)

  • Nouveau panneau BROADCASTERS entre PROFILS et TOKEN JOUEUR.
  • Dropdown inline des joueurs connectés non-broadcasters + bouton "Accorder". Se rafraîchit automatiquement sur join/leave et sur changement de la liste broadcasters. Pas de popup modale : le dropdown vit dans le panneau lui-même.
  • Croix par ligne pour révoquer (confirmation messagebox.askyesno).

Client (circusvoip_core.py + circusvoip_client.py)

  • Nouveau state : broadcast_all_key, broadcast_all_active, server_supports_broadcast_all, is_broadcaster.
  • Émission : nouvelle branche prioritaire dans _on_audio_captured (avant les checks canal/profil) qui émet une trame avec flag 0x03 (+ duplicate proximité 0x00 inchangé) quand la touche est maintenue et que le serveur supporte la capability.
  • Réception : _on_audio_received reconnaît le flag 0x03, bypass les filtres canal/profil, applique la chaîne radio (compression + effet). Reste soumis à mute_radio.
  • RadioKeyListener : nouveaux helpers _on_broadcast_pressed_impl / _released_impl, miroir du PTT profil. Force l'ouverture du gate audio et joue les bips locaux.
  • UI : nouvelle ligne de keybind « Diffusion globale (PTT) » dans les paramètres. Désactivée avec tooltip explicatif quand server_supports_broadcast_all == False (vieux serveur).
  • Auth client :
    • Helpers _get_broadcaster_token(ip, port) / _set_broadcaster_token(...). Stockage dans circusvoip_client_config.json sous broadcaster_tokens: {"host:port": "<token>"}. Per-server : un même client peut avoir des rôles différents sur plusieurs serveurs.
    • Le join présente automatiquement le token sauvegardé pour le serveur courant.
    • Handlers broadcaster_token_granted (sauve le token, log à l'utilisateur de se reconnecter pour activer la touche) et broadcaster_revoked (efface le token local).
    • Reasons d'erreur fatale interceptés : name_in_use, broadcaster_token_invalid → arrêt sans retry, l'utilisateur doit changer son nom ou demander un re-grant.

Modèle de permissions et flow

  • Rôle broadcaster distinct du rôle admin. Un broadcaster peut diffuser mais n'a aucun privilège admin.
  • Seul un admin peut grant/revoke.
  • Le grant exige que le joueur cible soit connecté au moment de la commande (sinon error: player_must_be_connected). C'est la seule fenêtre où le token peut être transmis (WS authentifiée, jamais en clair côté admin).
  • Latence de révocation : la capability est portée par le ticket audio (TTL 120s). Une révocation devient effective au prochain renouvellement de ticket, soit lors de la reconnexion du joueur au serveur positions.

Anti-impersonation

Le modèle player token partagé permettait à n'importe quel client avec le token de se déclarer sous n'importe quel nom au join (data.get("name", ...) sans vérification). Combiné avec la nouvelle feature broadcaster, ça donnait un trou d'usurpation : un attaquant pouvait se reconnecter sous le nom d'un broadcaster légitime et hériter de can_broadcast=True.

Cette PR ferme le trou avec deux mesures :

  1. Unicité de nom : refus si le nom est déjà utilisé par une connexion en cours (name_in_use).
  2. Token obligatoire pour les noms réservés : si le nom figure dans la liste des broadcasters, le client doit présenter le token correct. Sans token, le join est refusé — un attaquant ne peut donc pas non plus prendre le nom d'un broadcaster pour usurper son identité dans le chat / les canaux.

leochely added 7 commits May 18, 2026 21:21
issue() accepte maintenant des kwargs supplementaires (ex: can_broadcast) qui sont stockes tels quels dans l'entree du ticket. verify_full(), nouveau, retourne l'entree complete plutot que juste le nom : permet au serveur audio de lire les capabilities par-joueur fixees au moment de l'auth sur le serveur positions.

verify() (qui retourne juste le nom) est conservee pour les appelants existants - elle s'appuie maintenant sur verify_full() en interne.
Ajoute un role 'broadcaster' distinct du role admin : un joueur qui le porte peut emettre des trames audio avec le flag 0x03, relayees a tous les autres clients quels que soient leurs canaux radio. Cas d'usage : moderateurs et organisateurs d'evenements RP.

Positions server (circusvoip_server.py):
- _broadcasters: set persiste dans circusvoip_broadcasters.json
- Helpers grant_broadcaster, revoke_broadcaster, list_broadcasters
- Commandes admin associees dans _admin_handle_cmd
- Le ticket emis a l'auth porte can_broadcast=(name in _broadcasters)
- Le welcome ajoute 'server_caps': ['broadcast_all'] (capability discovery)
  + 'is_broadcaster' (etat actuel pour ce joueur)

Audio server (circusvoip_audio_server.py):
- state.client_caps stocke la capability lue dans le ticket au join
- Avant relais, drop des trames flag 0x03 dont l'emetteur n'a pas
  can_broadcast=True. Log rate-limite a 1/minute par ws pour ne pas
  flooder si un client tient la touche en continu.

Revocation : effective au prochain ticket (TTL <= 120s, soit ~2 min en pratique apres reconnexion du joueur au serveur positions). Documente dans grant_broadcaster()/revoke_broadcaster().
Ajoute un panneau BROADCASTERS dans la console admin (circusvoip_admin.py) entre PROFILS et TOKEN JOUEUR, miroir des panneaux existants : liste des broadcasters actuels avec un boutton + pour grant_broadcaster (askstring) et un croix par ligne pour revoke_broadcaster (confirmation). Aucun changement de protocole cote client joueur.

Cote serveur (circusvoip_server.py):
- admin_welcome contient maintenant 'broadcasters': sorted(_broadcasters)
- Nouveau push 'broadcasters_list' aux admins (pas a tous, contrairement aux canaux/profils : info admin)
- grant/revoke_broadcaster declenchent le push via _broadcast_broadcasters_list_threadsafe()

Test fixture .test_broadcast_all.py : start positions, attente cert.pem (size > 500), puis start audio. Evite la race condition ou les deux processus generent le cert auto-signe simultanement (corrupt PEM).
Cote core (circusvoip_core.py) :
- Nouveau state : broadcast_all_key, broadcast_all_active,
  server_supports_broadcast_all, is_broadcaster.
- _on_audio_captured : nouvelle branche prioritaire avant les checks
  canal/profil. Quand broadcast_all_active et le serveur le supporte,
  emet une trame flag 0x03 (+ duplicate proximite 0x00 inchange).
- Inbound handler : reconnait le flag 0x03, bypass les filtres
  canal/profil, applique la chaine radio (compression + effet). Toujours
  soumis a mute_radio.
- RadioKeyListener : nouveaux helpers _on_broadcast_pressed_impl /
  _released_impl, miroir du profil radio. Force l'ouverture du gate
  audio, joue les bips locaux.
- Dispatch dans _check_ptt_press / _check_ptt_release pour la touche
  broadcast_all_key.

Cote UI (circusvoip_client.py) :
- Handler welcome : lit server_caps + is_broadcaster, les stocke dans
  state.
- Nouvelle ligne de keybind "Diffusion globale (PTT)" dans les
  paramètres. Desactivee avec tooltip explicatif si
  server_supports_broadcast_all == False (vieux serveur).
- Persiste broadcast_all_key via _load_cfg / _save_cfg.

README : nouvelle section "Diffusion globale (rôle broadcaster)" + ligne
de keybind associee.
Le modele precedent autorisait n'importe quel client se presentant avec
le nom d'un broadcaster connu. Le shared player token + le self-declared
name ne suffisaient pas a empecher l'usurpation (un attaquant ayant le
player token pouvait se reconnecter en se nommant comme un broadcaster
legitime et heriter de can_broadcast=True).

Modele d'auth ajoute :
- circusvoip_broadcasters.json passe de list[str] a dict {name: sha256_hex}.
  Ancien format detecte au load : warning + liste videe (re-grant requis).
  On ne migre pas en silence : sans token push, le client cible n'aurait
  rien pour se re-authentifier.
- AuthRegistry est inchange. La verification se fait en amont, dans
  circusvoip_server.py via _hash_broadcaster_token / _verify_broadcaster_token
  (compare_digest pour eviter les leaks par timing).

grant_broadcaster est maintenant async et requiert que le joueur cible
soit connecte. Le serveur :
- Genere un token clair (16 octets d'entropie -> 32 hex chars).
- Stocke son sha256, jamais le clair.
- Push broadcaster_token_granted directement sur la WebSocket du joueur
  cible (canal authentifie deja par la connexion).
- Pousse la liste mise a jour aux admins (broadcasters_list).
Si le joueur est hors ligne : refus avec reason=player_must_be_connected.

revoke_broadcaster push broadcaster_revoked au joueur si connecte, pour
que son client efface le token local et ne tente plus de l'utiliser.

Join handler :
- Refuse si le nom est deja en ligne (name_in_use). Ferme aussi le
  scenario "se reconnecter sous un autre nom pendant que mon ancien WS
  est encore actif".
- Si le nom est dans _broadcasters, exige broadcaster_token verifie.
  Sans token (vide) ou avec un mauvais : refus broadcaster_token_invalid.
  Cela protege aussi l'identite : un non-broadcaster ne peut pas non
  plus prendre le nom d'un broadcaster connu pour usurper son identite
  dans le chat ou les canaux.
- can_broadcast n'est True dans le ticket que si le nom est broadcaster
  ET le token a ete verifie.

Console admin (circusvoip_admin.py) :
- "+ Ajouter" remplace simpledialog.askstring par un Combobox des joueurs
  actuellement connectes. Refleter la nouvelle contrainte cote serveur
  (player_must_be_connected) plutot que d'afficher une erreur apres coup.
Cote core (circusvoip_core.py) :
- Nouveau helpers _get_broadcaster_token / _set_broadcaster_token /
  _server_key. Stockes dans circusvoip_client_config.json sous la cle
  broadcaster_tokens : {"host:port": "<token>"}. Per-server : un meme
  client peut avoir des roles differents (ou n'en avoir aucun) sur
  plusieurs serveurs sans collision.
- state.broadcaster_token_for_current_server : champ d'etat (pas encore
  utilise UI, sert de cache interne).

Cote net (circusvoip_client.py) :
- NetWorker._ws_client lit le token sauvegarde pour (server_ip, SERVER_PORT)
  juste avant d'envoyer le join, et l'ajoute au payload comme
  broadcaster_token. Cle serveur memorisee pour les pushes ulterieurs.
- _handle_message : nouveau cas broadcaster_token_granted (sauve le token
  via _set_broadcaster_token, met is_broadcaster=True, previent l'UI via
  sig_log que l'utilisateur doit se reconnecter pour activer la touche).
- _handle_message : nouveau cas broadcaster_revoked (efface le token
  local, met is_broadcaster=False).
- _handle_message : deux nouveaux reason d'erreur fatale interceptes
  (name_in_use, broadcaster_token_invalid) -> stop_requested. Pas de
  retry auto : l'utilisateur doit changer son nom ou demander un re-grant.
L'ancien _add_broadcaster ouvrait une popup tk.Toplevel modale avec un
Combobox des joueurs connectes. Deux problemes :
- Reference des constantes inexistantes (FG, BG_BTN, BG_BTN_HOVER au lieu
  de TEXT, BORDER, BG_ROW) -> NameError silencieux -> popup vide.
- UX : un clic supplementaire et une popup juste pour selectionner un nom
  dans une liste qu'on a deja sous les yeux.

Refonte : le dropdown vit directement dans le panneau BROADCASTERS,
entre la liste des broadcasters et la section TOKEN JOUEUR. Plus de
popup ; juste Combobox + bouton "Accorder". Le dropdown auto-sync via
_refresh_add_broadcaster_dropdown() appele depuis refresh_players (pour
les join/leave) et refresh_broadcasters (pour les grants/revokes). Si
aucun joueur connecte n'est candidat -> dropdown disabled + vide.

_add_broadcaster (popup) supprime, remplace par _grant_selected_broadcaster
(action du bouton) qui envoie juste grant_broadcaster avec le nom selectionne.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant