1 Architecture
az4aaz edited this page 2026-09-21 19:36:57 +02:00

Architecture

flowchart LR
    N[Navigateur] --> P[PAFF, axum]
    P <--> R[(Redis<br/>sessions et cache)]
    P --> PASS[PASS / Alcuin]
    P --> M[Moodle]
    P --> I[Flux iCal]
    P --> S[Rapports SSRS]

Authentification

CAS, puis IdP Shibboleth, puis PASS (src/auth/mod.rs). Les cookies obtenus vont dans la session Redis.

Le pont ASP (init_asp_bridge) a son propre cookie jar. Fusionner ses cookies avec ceux de la session, sinon PASS répond "Session perdue".

Moodle a son propre flux via le WAYF (src/modules/moodle.rs), une dizaine de requêtes :

sequenceDiagram
    participant PAFF
    participant Moodle
    participant WAYF
    participant CAS
    PAFF->>Moodle: GET launch.php
    Note over PAFF,Moodle: pose tool_mobile_launch,<br/>obligatoire avant tout
    PAFF->>Moodle: GET shibboleth/index.php
    Moodle-->>WAYF: redirection
    PAFF->>WAYF: POST, choix de l'IdP
    WAYF-->>CAS: 4 redirections
    PAFF->>CAS: POST des identifiants
    PAFF->>CAS: POST du consentement Shibboleth
    Note over PAFF,CAS: _eventId_proceed,<br/>jamais _eventId_AttributeReleaseRejected
    PAFF->>Moodle: POST SAMLResponse à Shibboleth.sso
    Moodle-->>PAFF: moodlemobile://token=BASE64
    Note over PAFF: décodage SITE_ID:::TOKEN:::PRIVATE_TOKEN

La politique de redirection de reqwest s'arrête sur les schémas non HTTP : c'est ce qui permet d'attraper le token à l'étape 8.

Sources de données

Donnée Source Piège
Horaires, salles flux iCal jointure sur l'UID PASS-{NumEve}
Type et couleur HTML de Agenda.asp sans iCal, grille horaire, moins précise
Intervenants Eve-Det.asp
Notes export SSRS via ExportUrlBase HTML paginé : le premier <td> doit avoir la classe a145c
Cours API REST Moodle rapprochés des UE par recouvrement de mots
Annuaire DataGrid de Contenu.aspx voir plus bas
Feuilles d'émargement Moodle, cours 2579 une fiche nominative par étudiant, rangée par campus

L'appel DetEve(...) qui identifie un créneau est porté par onmouseover chez la plupart des comptes et par onclick chez d'autres. C'est un réglage PASS propre à chacun, que rien dans la page n'annonce. Lire tous les attributs on*.

Agenda.asp est piloté par le cookie ASP classique, pas par le jeton d'URL. Sans ce cookie, PASS redirige vers /commun/erreur.asp : quelques centaines d'octets sans aucun marqueur, qu'il ne faut pas lire comme une semaine vide.

L'adresse du flux iCal de chaque étudiant est distribuée par un webservice (hub.imt-atlantique.fr/passtools/.../ws-getICSlocation.php) qui la rend à partir du seul login, sans authentification, mais ne répond qu'au réseau de l'école : 403 à toute IP hors campus. Le serveur PAFF étant hébergé dehors, il ne peut pas la lire. Le flux lui-même est public : une fois l'adresse connue, PAFF la relit depuis n'importe où. C'est donc le navigateur de l'étudiant, sur eduroam ou VPN, qui interroge le webservice (le script en bas de templates/planning.html, tant qu'aucun flux n'est encore gardé) et repasse l'adresse à POST /api/ical/feed, qui la relit avant de la garder. Le jeton étant stable, une seule capture au premier passage sur le réseau école suffit pour toujours, et le flux resservira ensuite depuis le VPS. La page Profil permet toujours de coller l'adresse à la main.

GPA

Barème promo 2024 :

Note Intervalle Coefficient
A [17-20] 4.0
B [14-17[ 3.8
C [12-14[ 3.5
D [10-12[ 3.0
E rattrapage validé 2.5
Fx [5-10[ 0
F [0-5] 0

GPA = somme(coeff x ECTS) / somme(ECTS). ECTS codés en dur dans crates/paff-core/src/grades.rs.

Annuaire

  • App ID 142 (190 = agenda).
  • Charger tout le frameset avant que Contenu.aspx réponde :
flowchart LR
    A[Accueil.aspx] --> B[AnnTop.aspx] --> C[OngletsNavigation.aspx]
    C --> D[NavigationBase.aspx] --> E[Recherche.aspx] --> F[Contenu.aspx]
  • Changer d'annuaire : POST Recherche.aspx avec ddlAnnuaires=ID|30 et btnRecherche=Chercher. 134 = étudiants, 133 = personnels.
  • Pagination : __doPostBack('objets$Objets$_dataGridPager', 'N'), quotes encodées &#39;. NbLignes est ignoré par le serveur.
  • Photos publiques : /DataOp/Outils/Annuaire/{GUID}.jpg, GUID dans le src de #imgUcImage1 sur Dossier.aspx.
  • Les services SOPX renvoient "Access denied" à un étudiant, d'où le scraping.
  • La recherche replie les accents : le planning écrit "BERANGERE" ce que l'annuaire écrit "Bérangère".

Émargement

La feuille est imprimée, signée en séance et déposée en papier. PAFF supprime la recopie, pas la signature.

Le texte d'une fiche PDF ne sort pas dans l'ordre du flux : l'extraction passe par la position des glyphes (crates/paff-core/src/emargement/pdf.rs). Une valeur longue finit collée à son libellé (cellules centrées) ; le libellé étant un préfixe fixe, le reste de la ligne redonne la valeur. Une cellule trop étroite imprime deux lignes avec le libellé centré entre elles.

Le code UE se lit sur le bloc imprimé en haut de la fiche (emargement/ue_match.rs), qui renonce au premier doute. Le reste, l'étudiant l'apparie à un code de son bloc (emargement/pairing.rs), une fois pour toutes les semaines. Sa réponse compte aussi comme une voix par TAF : le code le mieux soutenu l'emporte, une égalité ne donne rien, sa propre réponse prime toujours. Le code est revérifié contre le bloc à chaque lecture et chaque écriture.

Remplissage : réécriture d'une seule entrée du zip, en chaînes littérales. Les durées sont formatées h:mm dans le classeur : écrire 3,5 afficherait une date de janvier 1900. L'unité est la minute.

Chaîne de fabrication :

flowchart LR
    T[Modèle xlsx<br/>assets/] --> F[Remplissage<br/>une entrée du zip]
    F --> X[Classeur xlsx]
    X --> L[LibreOffice<br/>service sheetengine]
    L --> P[PDF sans bandeaux]
    P --> H[letterhead.rs<br/>en-tête et pied réapposés]
    H --> S[PDF final]

Les images d'en-tête et de pied de page sont les seules à ne pas survivre à la conversion. Comme elles sont opaques, la page est d'abord rétrécie des deux bandes qu'il leur faut.

Le modèle embarqué (assets/feuille-emargement.xlsx) est dérivé de la version publiée par scripts/strip-emargement-template.py, qui retire aussi les chaînes partagées orphelines, le nom de feuille, les métadonnées d'auteur et le chemin réseau interne.

Les rattrapages d'une autre promo suivent la salle et atterrissent dans n'importe quel planning : retirés de la feuille, gardés dans le planning.

Signature manuscrite

La capture enregistre le geste, position et instant de chaque point, parce que les modèles cinématiques qui le rejouent travaillent sur le profil de vitesse. C'est la donnée la plus sensible du projet : elle n'est écrite dans Redis que chiffrée.

O'Reilly & Plamondon, Development of a Sigma-Lognormal representation for on-line signatures, Pattern Recognition 42(12), 2009. https://doi.org/10.1016/j.patcog.2008.10.017

XChaCha20-Poly1305 (crates/paff-core/src/signature/vault.rs), nonce de 192 bits tiré à chaque écriture, identifiant du propriétaire lié en données associées, si bien que déplacer une enveloppe vers un autre compte fait échouer le déchiffrement. Clé dans PAFF_SIGNATURE_KEY. L'enveloppe porte l'empreinte tronquée de la clé qui l'a scellée : ouvrir est une recherche dans le trousseau, pas une suite d'essais.

Ce que ça protège : ce que Redis écrit à partir du scellement (AOF, RDB, sauvegardes), plus une session redis-cli et une image disque. Ce que Redis a écrit avant reste en clair jusqu'au prochain BGREWRITEAOF, à lancer après le premier scellement, et à vérifier. Qui peut lire l'environnement du processus a la clé. Ce n'est pas du bout en bout.

Sans clé, une instance qui a ouvert la fonction refuse d'enregistrer (503) et le dit au démarrage.

Rotation : PAFF_SIGNATURE_KEY scelle, PAFF_SIGNATURE_KEY_PREVIOUS (virgules) ouvre encore. Le rescellement se fait à la lecture, sans migration ni re-signature. Retirer la clé sortante trop tôt ne perd rien : les enveloppes sont refusées, pas écrasées (SignatureLibrary::unreadable). Procédure complète dans .env.example.

Contraintes de code

  • scraper::Html et scraper::Selector utilisent des Rc : jamais à travers un .await. Tout le parsing HTML est synchrone.
  • paff-core ne dépend ni de reqwest, ni de tokio, ni d'axum.

Cache Redis

Clé Contenu TTL
session:{uuid} session utilisateur SESSION_TTL
events:{group}:{user}:{semaine} une semaine EVENT_CACHE_TTL
events:year:{group}:{user} l'année, pour /stats et l'émargement 24 h
detail:{event_id}:{date} prof et salle DETAIL_CACHE_TTL
grades:report:{user_id} relevé analysé GRADES_CACHE_TTL
grades:seen:{user_id} UE déjà notées 400 j
ects:{code} crédits lus chez FPC 30 j
annuaire:{dir} annuaire complet, partagé 6 h
emarg:header:{user_id} en-tête de fiche EMARGEMENT_HEADER_TTL
emarg:ue:{campus}:{taf} liste d'UE, partagée par TAF EMARGEMENT_UE_TTL
emarg:files:{course_id} listing du cours Moodle 1 h
emarg:ue-pairing:{user_id} appariements manuels 365 j
emarg:ue-shared:{campus}:{taf} une réponse par personne et par intitulé 365 j
ical_token:{token} / ical_user:{id} abonnement au flux exporté 7 j, prolongés à chaque lecture
signature:trace:{user_id} tracés chiffrés dernière échéance des gestes

Les événements sont indexés par utilisateur, pas par groupe : le NomCal (USR{user_id}) filtre différemment pour deux étudiants d'un même groupe.

Priorité à la page affichée

PASS traite les requêtes d'un utilisateur l'une après l'autre. Les travaux de fond (188 pages d'annuaire, l'année scannée, le catalogue FPC) empruntent sa session et passeraient devant la page qu'il demande. src/pass_priority.rs compte les requêtes attendues ; chaque travail de fond s'efface tant que ce compte n'est pas nul.

Le grain diffère : l'index de fiches et le catalogue cèdent avant chaque requête, l'annuaire par lots de six pages, le rattrapage des enseignants par quarante. Un lot commencé va au bout.

Les marqueurs "une tâche s'en occupe" (scan:lock:*, annuaire_lock:* et les autres) sont libérés au démarrage : un redémarrage tue les tâches, pas les marqueurs, et le travail restait refusé jusqu'à leur expiration.