Résumé

  • L’indice d’une table dynamique HPACK n’a de sens que dans le bon contexte, dans le bon sens de transmission et au bon instant d’une connexion HTTP/2.
  • Décoder une liste de champs n’est pas toucher un cache : stockage de la réponse, clé, fraîcheur, validation, autorisation et résultat applicatif exigent d’autres preuves.

Dans une trace réseau, 63 occupe moins de place que cache-control: private. Cette économie invite à une histoire trompeuse : le pair « connaissait déjà » le champ, donc la réponse aurait été retrouvée en cache. Entre ces deux propositions se trouve toute l’architecture HTTP.

HPACK mémorise des octets afin de ne pas les retransmettre. Un cache HTTP conserve une réponse afin de pouvoir satisfaire une requête ultérieure. Les deux mécanismes exploitent la répétition, mais ils ne gardent pas le même objet, n’obéissent pas à la même autorité et ne produisent pas la même preuve.

Roberto Peon a participé directement à cette séparation. Il a signé la RFC 7541 avec Hervé Ruellan. Son profil IETF répertorie aussi la RFC 7540, première spécification de HTTP/2. Une notice de conférencier QCon datant de 2012 le présente alors comme ingénieur chez Google et co-créateur de SPDY. Elle éclaire l’histoire ; elle ne prouve aucun poste actuel. La RFC 9113, texte HTTP/2 aujourd’hui en vigueur, a d’autres auteurs.

L’objet de HPACK est une liste ordonnée

La RFC 7541 décrit une liste de champs comme une collection ordonnée de couples nom-valeur. Les doublons sont permis. Pour le compresseur, noms et valeurs sont des suites d’octets opaques. L’ordre doit ressortir intact du décodage.

Cette modestie est une garantie. Le moteur HPACK ne décide pas ce que signifie private, n’évalue pas un ETag et ne choisit pas une langue. Il restitue les octets. Une couche HTTP ultérieure applique la grammaire et la sémantique du champ.

Un champ peut être transmis littéralement ou désigné par un indice. La table statique est prédéfinie et immuable. La table dynamique est propre à un contexte, vide au départ, alimentée au fil des blocs et gérée comme une file : la nouvelle entrée prend la première place dynamique, les plus anciennes glissent vers l’éviction. Deux entrées identiques peuvent coexister.

L’indice n’est donc pas le nom durable d’un fait. Une insertion change les positions. Une éviction supprime l’objet visé hier. Sur une autre connexion, le même nombre peut mener ailleurs ou être invalide. Écrire « indice 63 » sans l’état de table revient à noter un numéro de siège sans conserver le plan de la salle.

Deux sens, deux mémoires

Sur une communication bidirectionnelle, la RFC 7541 sépare entièrement les tables dynamiques d’encodage et de décodage d’un même terminal. Le contexte qui encode les requêtes n’est pas celui qui décode les réponses. Une seule connexion n’abolit pas cette frontière.

La RFC 9113 précise que chaque terminal possède un contexte encodeur et un contexte décodeur HPACK, utilisés par tous les blocs de champs de la connexion. La table dynamique en constitue l’état principal.

Pour une enquête, l’expression « la connexion contenait ce champ » ne suffit donc pas. Il faut le terminal, le sens, le numéro du bloc et l’état antérieur. Si le décodeur omet une insertion dans un bloc qu’il voulait jeter, le bloc suivant peut employer un indice que les deux côtés ne comprendront plus de la même façon.

C’est pourquoi un récepteur doit réassembler et décompresser un bloc complet même s’il ne compte pas utiliser le message. Le bloc peut modifier le dictionnaire dont dépend la suite. L’abandon applicatif d’un flux n’efface pas l’obligation de maintenir le contexte de compression.

La taille est négociée, pas archivée

À l’ouverture d’une connexion HTTP/2, la valeur initiale de la limite de table est de 4 096 octets. Le décodeur peut annoncer une autre limite avec SETTINGS_HEADER_TABLE_SIZE; l’encodeur choisit une taille effective qui ne la dépasse pas et la signale par une mise à jour HPACK.

Les 4 096 octets ne décrivent pas tous les produits du monde réel. Ils constituent un point de départ normatif. Une réduction dépend en outre de l’ordre des paramètres et de leur acquittement. Après l’acquittement, le prochain bloc doit porter la mise à jour exigée lorsque la table dépasse la nouvelle limite.

Chaque insertion peut chasser d’anciennes entrées. Une entrée trop grande vide la table au lieu de devenir un souvenir permanent. Rien ici ne ressemble au cycle de conservation d’une représentation HTTP.

La portée de l’erreur confirme la nature de l’état. Si un récepteur ne peut pas décompresser un bloc, la RFC 9113 impose une erreur de connexion COMPRESSION_ERROR. Le symptôme apparaît sur un flux, mais la divergence compromet le dictionnaire commun à tous les blocs de ce sens. La connexion doit s’arrêter ; cela ne signifie pas que l’application avait tort.

Le cache HTTP conserve une réponse

La RFC 9111 définit le cache comme un dépôt local de messages de réponse, accompagné du sous-système qui les stocke, les retrouve et les supprime. Son acte décisif consiste à réutiliser une réponse antérieure pour satisfaire une nouvelle requête.

Il faut pour cela une clé comprenant au minimum la méthode et l’URI cible. Les champs mentionnés dans Vary peuvent compléter la sélection. Puis viennent la fraîcheur, l’autorisation de servir un objet périmé, la validation auprès de l’origine et les directives telles que no-store, private ou no-cache.

Une table HPACK ne possède ni méthode, ni URI de sélection, ni horloge de fraîcheur, ni représentation à servir. Elle peut contenir les octets de etag: "bleu" sans conserver le corps associé. Elle peut compresser age: 300 sans calculer l’âge courant. Elle peut restituer vary: accept-language sans comparer les requêtes.

Prenons cache-control: private. L’encodeur peut indexer ce couple parce qu’il revient souvent. Cette insertion ne crée pas un cache privé et ne décide pas du droit d’un cache partagé. C’est le composant cache, après interprétation du message, qui applique la directive. La table de compression peut oublier le champ alors que la réponse demeure stockée ; elle peut aussi le garder alors qu’aucune réponse ne l’a été.

Inversement, un véritable accès au cache peut produire des champs littéraux, recourir à la table statique ou traverser une nouvelle connexion dont la table dynamique est vide. Les deux événements ne se prouvent pas l’un l’autre.

Un décodage réussi reste une preuve étroite

Un indice correctement décodé établit qu’à cet instant les deux côtés disposaient d’un état de compression compatible. Le décodeur a pu reconstituer un couple de champs dans la bonne séquence. Il peut poursuivre le traitement.

Il n’a pas pour autant validé les caractères autorisés, la sémantique du champ, l’identité de l’origine ou l’action de l’application. Un intermédiaire peut avoir transformé le message. Le serveur d’origine peut ne jamais avoir vu la requête. La couche supérieure peut rejeter une valeur parfaitement décompressée.

La confidentialité ajoute une autre limite. La RFC 7541 explique qu’un attaquant capable d’influencer des champs et d’observer leur longueur comprimée peut sonder l’état de la table. TLS dissimule le contenu mais pas toute information de longueur. Un meilleur taux de compression n’est donc pas toujours une mesure innocente.

La représentation littérale « jamais indexée » ordonne aux intermédiaires de préserver ce traitement lors d’un réencodage. Elle évite d’insérer la valeur dans la table. Elle ne rend pas un secret peu entropique impossible à deviner, ne supprime pas toutes les fuites de longueur et ne remplace pas la politique applicative.

Deux reçus, pas un indicateur vert

Le reçu HPACK doit nommer la connexion, les rôles des terminaux, le sens, le flux et l’ordre des blocs. Il conserve la limite annoncée, l’instant de l’acquittement, la taille effective avant et après, les insertions, évictions et changements, la forme littérale ou indexée et le résultat du décodage.

Le reçu HTTP commence après cette reconstruction. Il note la méthode, l’URI, le statut, le chemin d’origine et la sémantique analysée. Si un cache intervient, il ajoute la clé, les entrées Vary, la décision de stockage, l’âge, la fraîcheur, les validateurs, les règles liées à l’autorisation et le résultat de la revalidation.

Une capture réseau peut prouver la synchronisation HPACK sans rien savoir du cache. Un journal de cache peut prouver la réutilisation sans exposer la compression sur le fil. Les cases absentes doivent rester vides. Le fait qu’un décodeur se soit souvenu d’octets n’autorise jamais, à lui seul, la réutilisation d’une réponse.

Sources