-
Notifications
You must be signed in to change notification settings - Fork 31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Historisation des ressources : support content-disposition en latin1 #3988
Conversation
Les headers HTTP sont encodés en latin1 (iso-8859-1), les chaînes de caractères elixir sont encodés en UTF-8. L‘on pourrait croire que la bibliothèque HTTP cliente décoderait de latin1 vers utf8 et fournirait des chaînes de caractères systématiquement mais ça ne semble pas être le cas. HTTPoison produit le même résultat pour l‘URL de la ressource mentionnée dans l‘issue. Quand le contenu d‘un header ne rentre pas dans le subset ASCII (commun à latin1 et utf8), Req renvoie un binary que l‘utilisateur de la bibliothèque devra lui-même décoder. Cette pull request se propose de faire cela pour l‘historisation de ressources. Ma compréhension (partielle) est qu‘il s‘agit d‘un bug (ou a minima un défaut de Req et HTTPoison). Si d‘autres que moi pensent que c‘est en effet un défaut je logguerai une issue sur Req ou HTTPoison (ou les deux). Closes #3984.
Étonnant tout ça. Mon
Quel est le bon comportement ? Est-ce que les headers de réponse HTTP renvoyés sont réellement valides ? |
Tu me mets le doute. J'avais débugué avec httpie rapidement au début sans observer de défaut de décodage via cet outil, mais pas testé avec curl. Je vais revérifier avec d'autres clients HTTP. |
J'ai un serveur qui renvoie un ❤️ en valeur de header HTTP et ça passe avec HTTPoison mais pas pour le iex> HTTPoison.get!("https://pysae.com/api/v2/groups/st-tropez/gtfs/pub").headers
[
{"Date", "Tue, 11 Jun 2024 15:31:44 GMT"},
{"Content-Type", "application/zip"},
{"Transfer-Encoding", "chunked"},
{"Connection", "keep-alive"},
{"cache-control", "max-age=10368000"},
{"content-disposition",
<<97, 116, 116, 97, 99, 104, 109, 101, 110, 116, 59, 32, 102, 105, 108,
101, 110, 97, 109, 101, 61, 78, 97, 118, 101, 116, 116, 101, 95, 83, 97,
105, 110, 116, 95, 84, 114, 111, 112, ...>>},
{"Strict-Transport-Security", "max-age=15724800; includeSubDomains"}
]
iex> HTTPoison.get!("https://ansart-augusti.fr").headers
[
{"Server", "nginx"},
{"Date", "Tue, 11 Jun 2024 15:32:22 GMT"},
{"Content-Type", "text/html"},
{"Content-Length", "178"},
{"Connection", "keep-alive"},
{"Location", "https://www.ansart-augusti.fr/"},
{"X-XSS-Protection", "1; mode=block"},
{"X-Content-Type-Options", "nosniff"},
{"Referrer-Policy", "no-referrer-when-downgrade"},
{"Content-Security-Policy",
"default-src 'self' http: https: data: blob: 'unsafe-inline'; frame-ancestors 'self';"},
{"Permissions-Policy", "interest-cohort=()"},
{"x-message", "❤️"}
] |
La version web de httpie me montre le défaut d'encodage. Je me demande si le résultat dans mon terminal n'est pas un artefact (positif) de mon terminal lui-même. A noter que je reproduis ton observation via curl. Si je devais parier je dirais qu'il y a de l'encodage Windows-1252 dans cette histoire... |
En relisant https://datatracker.ietf.org/doc/html/rfc5987 qui rappelle que par défaut les en-têtes sont encodés en latin1, il est possible de choisir un encodage différent par header selon un syntaxe relativement récente, mais qui n'est pas employée par pysae dans notre cas. |
Il est possible que le nom de fichier soit repris à partir du nom de fichier de l'utilisateur ayant déposé quelque chose sur la plateforme, depuis un poste Windows. Peux-tu contacter Pysae et partager ce bug ? |
xh m'affiche ceci:
Ce qui confirme que l'encodage est curieux. Je ne peux plus faire confiance à httpie... |
je m'en occupe |
Les points suivants me chaffouinent :
|
Il me manquait le bucket en local ; c'est corrigé et l'historisation passe avec ce changement. Je n'explique pas encore pleinement le bien fondé du patch. Je dois creuser dans l'implémentation HTTP sous-jacente pour le justifier. |
Le bug actuel survient quand on cherche à stocker la payload en JSON, qui contient une allowlist de K/V de headers HTTP et là Jason a du mal à bien encoder la valeur mal encodée. La survenance du bug montre un problème potentiel pour les réutilisateurs. Idéalement le producteur devrait résoudre le bug. Par contre difficile de choisir entre fixer le bug (encodage pour tout ce qui passe) ou avoir une meilleure détection d'un tel problème pour gérer. Il est aussi possible que certains producteurs ne corrigent jamais ou très lentement, ce qui bloque une fonctionnalité importante (historisation > métadonnées > validation) |
Mise à jour de ma compréhension du problème: La présence éventuelle de binary pour certaines valeurs de header pose problème en aval (lors du logging notamment, mais j'imagine aussi pour l'encodage en JSON). En pratique cette PR prend en charge la présence d'un binary en le décodant comme étant du latin1 (ce qui n'est pas 100% rigoureux ; il est possible que certains binary soient encodés différemment). |
Voir elixir-mint/mint#301 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sujet pas simple 😄
J'ai fini par tomber sur ce chouette article https://www.bortzmeyer.org/6266.html et ouvrir un bug report (postmanlabs/httpbin#724).
Vu que la version actuelle du fix touche 100% des headers sur 100% des historisations, le rapport ennuis sur bénéfices est embêtant (même après correctif je trouve), ça nécessiterait pour être sûr de ne rien casser sur cette partie assez critique des tests sur le volume (ce qui a déjà été fait et peut se faire, mais prend un peu de temps).
Et je me suis demandé aussi quelle est la proportion de serveurs qui vont servir du HTTP2 actuellement aussi, où le cas est géré assez différemment (les headers en UTF-8 ne posent pas de souci si j'ai compris, mais on peut rencontrer d'autres choses).
Toutefois, vu qu'on a qu'un seul cas, et que @etalab/transport-bizdev a des contacts chez Pysae, je préconise qu'on voit avec un biz-dev pour demander une correction chez eux (passer été
en ete
), et suivre ainsi ce que j'ai lu ici:
https://tools.ietf.org/html/rfc6266#appendix-D
To successfully interoperate with existing and future user agents,
senders of the Content-Disposition header field are advised to:
SNIP
Avoid using non-ASCII characters in the filename parameter.
Although most existing implementations will decode them as
ISO-8859-1, some will apply heuristics to detect UTF-8, and thus
might fail on certain names.
end | ||
|
||
defp decode_header(as_latin1) when is_binary(as_latin1) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is_binary
va retourner true
dans tous les cas qui nous intéressent, et comme le dit @AntoineAugusti plus bas, ça va donc matcher tout le temps si je lis bien.
On a en effet:
> is_binary(<<233>>)
true
> is_binary("é")
true
Une String est toujours un binary, mais tous les binaries ne sont pas des String, car (doc):
Strings in Elixir are UTF-8 encoded binaries.
On peut vérifier si un binary est un string avec:
https://hexdocs.pm/elixir/1.17.0/String.html#valid?/2
Là dans le cas présent, ça veut dire que:
- la translitération va être appelée sur tous les headers, tout le temps
- et si on avait un header en UTF-8 (String) valide avec des accents (ce qui peut arriver en HTTP 2), le code de translitération va causer un souci, en réencodant
"é"
(UTF-8) en"é"
(EDIT: j'ai créé un test-case plus haut)
Et sinon, je propose un hot-fix beaucoup plus ciblé, si Pysae ne peut pas bouger:
> :binary.replace("attachment" <> <<102, 105, 108, 101, 110, 97, 109, 101, 61, 233, 46, 122, 105, 112>>, << 233 >>, "é")
"attachmentfilename=é.zip" (et pareil pour le C'est au scotch, mais beaucoup plus facile à recetter et moins d'impact. |
Je vais me ranger à la position prudente et remplacer les caractères inattendus le temps que ça soit corrigé chez l'AOM. Je les ai contactés par le formulaire en ligne, je vais tenter par le contact direct si ça répond pas. |
Cependant, Pour l'instant je n'ai lu aucun argument donnant tort à Pysae. Ils seraient en droit de répondre que leur réponse HTTP respecte la spec. Les headers HTTP1.1 sont en latin1, ils renvoient des caractères latin1. A ce titre je continue de penser que mon fix est le bon et que les client HTTP devraient faire ce décodage à notre place systématiquement. La posture de mint "ça devrait être du ASCII-7 partout" couplée à "HTTP2 est en UTF-8, la lib est récente, on va pas supporter de vieux comportements" me déçoit fortement :/ |
Ah oui je ne leur donne pas du tout tort non plus, je précise, même si je ne l'avais pas écrit. |
Si j'ai compris le fait de ne pas mettre de caractères non ASCII-7 est également une recommandation forte, donc ils y ont été à l'économie, sur un écosystème de petite taille. Mais je n'ai pas encore d'avis clair là dessus, je creuserai à l'occasion par curiosité ! |
Mes derniers changements ( |
parce que je n'ai pas fait un "replace all" avec l'option |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, en espérant qu'on puisse retirer ce fix un jour.
Peut-être tenter de contacter de nouveau Pysae en trouvant un contact tech sur LinkedIn ?
Je peux tenter le contact de Stéphane comme suggéré par Thibaut. Plus je regarde ce bug et moins je pense que Pysae est en tort. Ils sont peut-être à contre courant mais difficile de leur reprocher de respecter la RFC et je ne m'attends pas forcément à une réponse positive de leur part. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Le fix est beaucoup plus clean, GG le fait de vérifier d'abord avec String.valid?
pour ne pas corrompre les headers accentués déjà en UTF-8!
Après relecture et petite vérification, je me suis aperçu de plusieurs choses:
-
on a des coquilles dans nos fixtures: le header
content-disposition
n'autorise pasattachment, filename=this.zip
(avec une virgule), la bonne version estattachment; filename="this.zip"
avec un point-virgule (on peut laisser les double-quotes de côté je pense et le serveur Pysae ne le respecte pas, mais par contre il faut;
, voir https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) -
%Req.Response{headers: %{"content-disposition" => ["attachment", "filename=éè.zip"]}}
ne correspond pas à la réalité de ce que retourneReq
sur un cas correct. Une seule chaîne est retournée (dans le cas ce cet exemple, ça sera"attachment; filename=éè.zip"
). Je propose de modifier les fixtures pour que ça colle à la réalité. -
la version actuelle du code applique le traitement (chemin de code, avec ou sans transcodage) à tous les
@headers_to_keep
, je crois que le cas ne se produit que pourcontent-disposition
(vu que ça contient un nom de fichier etc), ou bien il faudrait ne faire ce hotfix que pour le header spécifique, ou accepter un petit risque (probablement mineur à mon avis) d'impact sur des headers non concernés, et le fait que ça consomme un peu + de processing mais ça à cette échelle c'est pas gênant -
je remarque dans l'output de curl sur le flux Pysae:
❯ curl -i https://pysae.com/api/v2/groups/st-tropez/gtfs/pub
HTTP/2 200
# SNIP
content-disposition: attachment; filename=Navette_Saint_Tropez_(horaires_?t?_2024).zip
- le retour est marqué comme
HTTP/2
- le header
content-disposition
est encodé enlatin-1
détail pour l'aspect latin-1
# merci Frédéric
❯ curl --dump-header st-tropez-headers.txt https://pysae.com/api/v2/groups/st-tropez/gtfs/pub -o /dev/null
# sur Mac
❯ file st-tropez-headers.txt
st-tropez-headers.txt: ISO-8859 text, with CRLF line terminators
# caractères 0xe9 = "é" en ISO-8859-1 = Latin-1
❯ xxd st-tropez-headers.txt | grep horaires
000000b0: 5f28 686f 7261 6972 6573 5fe9 74e9 5f32 _(horaires_.t._2
Je ne sais pas actuellement statuer de façon certaine sur le côté obligatoire ou pas de UTF-8 pour les headers qui ne spécifient pas leur encoding en HTTP/2, mais ça m'intrigue et j'avais lu des choses dans ce sens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ça me paraît nickel, bravo !
(petite suggestion d'amélioration d'un dernier test mais c'est tout)
J'ai fait des petits tests sur de la data, je pense qu'on peut envoyer !
Merci pour le fix et le suivi
@ptitfred je me suis permis de commit le dernier bout de test direct pour qu'on puisse envoyer. Je te laisse merger quand tu veux ! |
Les headers HTTP sont encodés en latin1 (iso-8859-1), les chaînes de caractères elixir sont encodés en UTF-8. L‘on pourrait croire que la bibliothèque HTTP cliente décoderait de latin1 vers utf8 et fournirait des chaînes de caractères systématiquement mais ça ne semble pas être le cas. HTTPoison produit le même résultat pour l‘URL de la ressource mentionnée dans l‘issue.
Quand le contenu d‘un header ne rentre pas dans le subset ASCII (commun à latin1 et utf8), Req renvoie un binary que l‘utilisateur de la bibliothèque devra lui-même décoder. Cette pull request se propose de faire cela pour l‘historisation de ressources.
Ma compréhension (partielle) est qu‘il s‘agit d‘un bug (ou a minima un défaut de Req et HTTPoison). Si d‘autres que moi pensent que c‘est en effet un défaut je logguerai une issue sur Req ou HTTPoison (ou les deux).
Closes #3984.