Protocole HFR
Endpoints, form fields, constantes et edge cases du protocole HFR utilisés par Redface 2.
Préambule
HFR n’expose aucune API publique. Le client Redface 2 fait du scraping HTML sur les pages du forum, avec gestion de session par cookies. Cette page documente les invariants du protocole — constantes, form fields, anti-CSRF, anti-bot, optimisations JS inline — que le LLM qui écrit le parser ou le client réseau doit respecter.
Cette documentation est issue de la rétro-ingénierie du code de Redface v1 (Java + Retrofit 1.9) et de fixtures HTML réelles capturées depuis forum.hardware.fr.
Endpoints par fonctionnalité
| Fonctionnalité | Méthode | Endpoint | Auth requise |
|---|---|---|---|
| Page d’accueil (catégories) | GET | /hfr/ ou / | non |
| Liste de topics d’une sous-catégorie | GET | /forum2.php?config=hfr.inc&cat={cat}&subcat={subcat}&page={page} | non (logué ↔ lu/non-lu visible) |
| Liste topics (rewrite SEO) | GET | /hfr/{cat_slug}/{subcat_slug}/liste_sujet-{page}.htm | non |
| Lecture d’un topic | GET | /forum2.php?config=hfr.inc&cat={cat}&post={post}&page={page} | non |
| Drapeaux (accueil Redface 2) | GET | /forum1f.php?config=hfr.inc&owntopic={filter_id} | oui |
| Login | POST | /login_validation.php?config=hfr.inc&redirect=&url= | — |
| Reply (post) | POST | /bddpost.php?config=hfr.inc&cat={cat} | oui |
| Edit (post) | POST | /bdd.php?config=hfr.inc&cat={cat} | oui |
| Edit FP (premier post) | POST | /bdd.php?config=hfr.inc&cat={cat} avec champ spécifique | oui |
| Nouveau topic | POST | /bddpost.php?config=hfr.inc&cat={cat}&subcat={subcat}&new=1 | oui |
| MP (envoi) | POST | /bddpost.php?config=hfr.inc&cat=prive&pseudo={dest} | oui |
| Conversation MP | GET | /message.php?config=hfr.inc&cat=prive&post={mp_id}&page={page} | oui |
| Liste des MPs | GET | /message.php?config=hfr.inc | oui |
| Page d’édition d’un post | GET | /message.php?config=hfr.inc&cat={cat}&post={post}&numreponse={numreponse} | oui |
| Ajouter aux drapeaux | GET | /user/addflag.php?config=hfr.inc&cat={cat}&post={post}&numreponse={numreponse} | oui |
| Retirer des drapeaux | GET | /user/delflag.php?config=hfr.inc&cat={cat}&post={post}&p=1&sondage=0&owntopic={0,1}&new=0 | oui |
| Profil public | GET | /hfr/profil-{user_id}.htm | non |
| Paramètres utilisateur | GET | /editprofil.php?config=hfr.inc&page={1..7} | oui |
| Modération (alerte) | GET/POST | /modo.php?config=hfr.inc&cat={cat}&post={post}&numreponse={numreponse} | oui |
| Recherche | GET | /search.php?config=hfr.inc&search={query}&cat={cat}&... | non |
Note sur
PRIVATE_MESSAGE_CAT_ID: la catégorie des MPs est la chaîne"prive"et non un entier. Attention lors du typage côté Kotlin —cat: Stringpour les endpoints MP ou sentinel dédié.
Form fields critiques
POST bddpost.php (reply ou nouveau topic)
| Field | Valeur | Obligatoire | Description |
|---|---|---|---|
hash_check | <token> extrait de la page GET précédente | oui | Anti-CSRF. Voir section dédiée. |
verifrequet | "1100" | oui | Constante anti-bot. String, pas entier. |
cat | ID catégorie | oui | ou "prive" pour MP |
post | ID topic | oui si reply | Absent si nouveau topic |
MsgIcon | "1" | conventionnel | Icône du message (1 = défaut) |
signature | "1" | conventionnel | Inclure la signature |
wysiwyg | "0" | conventionnel | Mode BBCode brut |
new | "0" ou "1" | oui | "1" pour nouveau topic |
page | "1" | conventionnel | |
p | "1" | conventionnel | |
sondage | "0" ou "1" | oui si topic | "1" si nouveau topic avec sondage |
owntopic | "0" ou "1" | oui | Topic favori ? |
config | "hfr.inc" | oui | Toujours hfr.inc pour HFR |
content_form | contenu BBCode | oui | Le corps du message |
subject | titre | oui si nouveau topic | Absent si reply |
POST bdd.php (edit)
| Field | Valeur | Description |
|---|---|---|
hash_check | <token> | Anti-CSRF |
verifrequet | "1100" | Anti-bot |
cat | ID catégorie | |
post | ID topic | |
numreponse | ID post | Post à éditer |
content_form | nouveau contenu BBCode | |
subject | nouveau sujet | Seulement si edit FP |
pollsondage | données sondage | Seulement si edit FP avec sondage |
Le fait qu’une édition concerne le premier post (FP) vs un post normal est déduit côté client (isFirstPostOwner) puis pris en compte dans la construction du form.
POST login_validation.php
| Field | Valeur | Description |
|---|---|---|
pseudo | username | |
password | password (plaintext) | Attention : HFR attend le password en clair dans le form POST (over HTTPS). Ne pas hasher côté client. |
Détection du succès : cookie md_user présent dans la réponse. Détection de l’échec : pattern Votre mot de passe ou nom d'utilisateur n'est pas valide dans le body HTML.
Constantes anti-CSRF et anti-bot
hash_check — anti-CSRF
Chaque page HFR qui autorise un POST (édition, création, action sur le profil) embarque un champ caché dans le DOM :
<input type="hidden" name="hash_check" value="<token>" />
Règle : avant tout POST, le client doit :
- Faire un GET sur la page d’édition/création appropriée.
- Extraire
hash_checkvia la regex ou sélecteur CSSinput[name="hash_check"]. - Injecter la valeur dans le form POST.
- Si
hash_checkest absent du DOM → fail fast visible (erreur explicite, pas silencieuse). Le POST ne doit jamais partir sans.
En v1, le code HashcheckExtractor.java utilisait la regex suivante — à reproduire ou équivalent Jsoup :
val hashCheck = document.select("input[name=hash_check]").attr("value")
require(hashCheck.isNotBlank()) { "hash_check absent — le POST serait silencieusement rejeté par HFR" }
verifrequet = "1100"
Constante anti-bot statique, présente dans tous les POST vers HFR. Valeur littérale "1100" (string, pas un nombre dynamique).
En Kotlin, à constanter dans :core:network :
object HfrConstants {
const val VERIF_REQUET = "1100"
const val CONFIG = "hfr.inc"
const val PRIVATE_MESSAGE_CAT = "prive"
}
numreponse — unique par catégorie, pas globalement
Le numreponse d’un post est unique au sein d’une catégorie (cat=X). Deux posts dans deux catégories différentes peuvent avoir le même numreponse. Le triplet (cat, post, numreponse) est unique globalement.
Conséquences pour le code
-
Base Room :
numreponseseul n’est pas une clé primaire valide. Utiliser une clé composite :@Entity( tableName = "posts", primaryKeys = ["cat", "numreponse"], indices = [Index(value = ["cat", "post"])], ) data class PostEntity( val cat: Int, val post: Int, // topic ID val numreponse: Int, // ... ) -
Deep linking : toujours inclure
catETnumreponse(optionnellementpostpour la page). Un deep link qui ne fournit qu’unnumreponseest ambigu. -
Recherche : les résultats contiennent toujours
(cat, post, numreponse)— ne pas perdrecatquand on stocke un résultat pour navigation ultérieure.
listenumreponse — optimisation JS inline
Chaque page topic HFR embarque un script inline du type :
<script type="text/javascript">
var listenumreponse = new Array();
listenumreponse[0] = 1234567;
listenumreponse[1] = 1234570;
// ...
listenumreponse[39] = 1234999;
</script>
Ce tableau contient les numreponse des posts de la page courante. Redface v1 ne l’utilisait pas. Opportunité pour v2 :
- Identifier rapidement quels posts sont sur une page sans parser tous les blocs HTML
- Détecter si un
numreponseattendu (ex : après un reply) est présent dans la réponse - Optimiser le prefetch et la réactualisation
Extraction recommandée :
val listeNumreponse: List<Int> = Regex("""listenumreponse\[\d+\]\s*=\s*(\d+)""")
.findAll(html)
.map { it.groupValues[1].toInt() }
.toList()
Voir le skill /parse-fixture (étape 3) pour la procédure d’extraction des variables JS inline.
cryptlink — protection des URLs externes
HFR peut “crypter” certaines URLs externes (anti-scraping, tracking). Redface v1 ne gère pas explicitement ce cas (pas de transformer dédié) — les URLs sont simplement relayées au navigateur.
Décision v2 : idem v1 — ne pas déchiffrer, relayer en l’état. Si l’UX en pâtit, revoir cette décision en Phase 2 via un transformer dédié.
Smileys
Deux sources distinctes :
| Type | URL de base | Exemple |
|---|---|---|
| Smileys built-in HFR | https://forum-images.hardware.fr/icones/ | :jap: → /icones/smilies/jap.gif |
| Smileys custom utilisateur | https://forum-images.hardware.fr/images/perso/{user_id}/ | Upload par l’utilisateur via editprofil.php?page=5 |
Règles de rendu :
- Cache Coil agressif (les smileys ne changent jamais) :
CachePolicy.ENABLED+ disque infini - Extraire le code smiley (
:jap:,:bounce:) depuis l’attributaltde l’<img>pour pouvoir le re-saisir côté éditeur - Les smileys custom d’un utilisateur sont exposés dans son profil (section
perso) - Un catalogue “wiki smileys” est disponible via
message-smi-mp-aj.php(recherche de smileys)
Sessions, cookies et 403
Cookies HFR
| Cookie | Rôle | Durée |
|---|---|---|
md_user | ID utilisateur — indicateur de session active | 1 an |
md_pass | Token de session | 1 an |
md_forum | Identifiant de forum | session |
| Cookies divers (tracking interne HFR) | — | variable |
Indicateur de session active : présence du cookie md_user. Si une réponse HTTP redirige vers la page de login ou si le DOM ne contient plus le pseudo de l’utilisateur connecté, la session a expiré.
Détection et recovery de session expirée
Un Interceptor OkHttp :
- Détecte HTTP 302 vers
/login.phpou absence du pseudo dans la réponse. - Émet un événement
SessionExpired. - Le
NavGraphredirige vers l’écran de login et efface le cache Room.
L’utilisateur ré-entre son mot de passe (Option A : pas de re-login transparent, le password n’est pas stocké — voir architecture.md#stockage-sécurisé-des-credentials).
403 / rate limiting
HFR n’expose pas officiellement de header Retry-After. En pratique :
- HTTP 429 ou 403 sur des requêtes répétées → backoff exponentiel (2s, 4s, 8s, max 60s)
- Rate limit client-side : max 2 requêtes/s sur les endpoints POST (reply, edit, flag)
- Aucune information officielle sur les seuils HFR — ces valeurs sont empiriques
Règle critique : prefetch non-authentifié
Les requêtes de prefetch (pages suivantes d’un topic, drapeaux préchargés) ne doivent jamais inclure les cookies de session HFR.
Raison : HFR met à jour les drapeaux (topics marqués comme lus) sur toute requête authentifiée. Un prefetch avec session marquerait silencieusement les topics comme lus, ce qui est exactement le bug que Redface v1 présentait.
Implémentation :
class NetworkModule {
@Provides @AuthenticatedClient
fun provideAuthClient(@UserCookieJar jar: CookieJar): OkHttpClient = /* ... */
@Provides @AnonymousClient
fun provideAnonymousClient(): OkHttpClient = OkHttpClient.Builder()
.cookieJar(CookieJar.NO_COOKIES) // ou un CookieJar vide dédié
.build()
}
class HfrClient @Inject constructor(
@AuthenticatedClient private val auth: OkHttpClient,
@AnonymousClient private val anon: OkHttpClient,
) {
suspend fun fetchTopicPage(cat: Int, post: Int, page: Int): String = /* auth */
suspend fun prefetchTopicPage(cat: Int, post: Int, page: Int): String = /* anon */
}
Un test Konsist enforce la règle : tout appel à prefetch* doit utiliser @AnonymousClient.
Confirmé par Corran Horn sur le topic HFR Redface 2 : « en utilisant un cookie d’un compte anonyme pour pas péter les drapeaux ».
Autres edge cases documentés
Posts édités
Pattern dans le HTML des posts : Message édité par <auteur> le DD-MM-YYYY à HH:MM:SS. Extraire côté parser en champ Post.editedAt: Instant?.
Posts supprimés / modérés
Structure HTML altérée : le <table class="messagetable"> peut ne plus contenir que le bandeau d’auteur + une mention de suppression. Le parser doit gérer ce cas sans crasher — Post.content devient vide ou marqueur sentinel "[Message supprimé]".
Emails obfusqués
HFR obfusque les emails dans les profils publics. Le texte visible est souvent "Vous n'avez pas accès à cette information" ou un email brouillé. Ne pas tenter de déobfusquer — conserver la string brute.
Pagination edge case
Si la meta description HTML Pages : N est absente ou malformée, utiliser UNKNOWN_PAGES_COUNT = -1 (sentinel) et recalculer côté client en naviguant. Fixture topic_last_page.html couvre ce cas (page partielle avec moins de 40 posts).
postsPerPage configurable
Le nombre de posts par page est un réglage utilisateur HFR (editprofil.php?page=3), pas une constante. Ne jamais hardcoder 40 dans le code. Le parser lit la valeur depuis la page de paramètres à la connexion et la stocke dans UserSettings.postsPerPage.
Fixtures HTML
Les fixtures de test du parser vivent dans core/parser/src/test/resources/fixtures/ (à créer en Phase 0). Chaque fixture doit être :
- Capturée depuis HFR réel (jamais fabriquée par une IA ou à la main)
- Nettoyée des données sensibles avant commit : cookies,
hash_check, emails, identifiants réels, URLs signées - Annotée avec sa source HFR (URL ou
cat=X, post=Y, numreponse=Z) dans un fichier.source.txtfrère ou en commentaire en tête du HTML
Catalogue complet : voir contributing.md#fixtures-html-pour-le-parser.
Pour capturer une fixture : utiliser le MCP hfr-mcp avec hfr_read output=path/to/fixture.html (écrit le HTML brut), puis appliquer le skill /parse-fixture pour générer l’analyse structurée.
Sources
- Redface v1 code
- Redface v1 fixtures
- MesDiscussions SDK (Wayback Machine) — ancienne doc partielle des paramètres URL HFR
- Skill
/parse-fixturepour l’analyse d’une fixture - MCP
hfr-mcppour interagir avec forum.hardware.fr depuis les agents LLM