Merge branch 'fix/qemu_cache'

[FIX] qemu cache: git mirror limited to http(s), icons on UI chooser

3 commits. The git mirror of the QEMU cache took the scheme of a git
negotiation from the request line the client writes, so an ssh:// or
git:// repository made the cache connect, with its service account keys,
to a host the client chose. It now clones only over http and https, and
the binary reports 0.2.17. The interface chooser of the TODO CLI gains
icons on its two choices.
Checked: full qemu_cache Go suite passes, a real clone through the
mirror still works; test_todo_i18n passes.

--- FR ---

[FIX] qemu cache : miroir git limité à http(s), icônes du choix d'IU

3 commits. Le miroir git du cache QEMU prenait le schéma d'une
négociation git dans la ligne de requête écrite par le client : un dépôt
ssh:// ou git:// faisait se connecter le cache, avec les clés de son
compte de service, à un hôte choisi par le client. Il ne clone plus qu'en
http et https, et le binaire annonce 0.2.17. Le choix d'interface du CLI
TODO gagne des icônes sur ses deux options.
Vérifié : suite Go de qemu_cache au vert, un vrai clone à travers le
miroir fonctionne toujours ; test_todo_i18n passe.

Assisted-by: Claude Opus 5.5
This commit is contained in:
Mathieu Benoit 2026-09-25 05:40:28 -04:00
commit 1f4efee16b
7 changed files with 77 additions and 9 deletions

View file

@ -178,6 +178,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Dependabot groups `aiobotocore`, `botocore` and `boto3` into one pull request, since each aiobotocore accepts only a narrow botocore range; security fixes still arrive on their own
- `TODO › Transform data` reads Excel with openpyxl 3.1.5 and xlsxwriter 3.2.9; the leak test that guards openpyxl's exact pin passes on them
- factur-x requires 6.8 outside s390x, the version already locked, so a regeneration can no longer fall back to an untested 4.x or 5.x
- The interface chooser of the QEMU deployment and of the Odoo migration, and its preferences in `TODO › Configuration`, mark the TUI form with 📋 and the line by line questions with 💬
<!-- [fr] -->
@ -212,6 +213,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Dependabot réunit `aiobotocore`, `botocore` et `boto3` dans une seule demande de fusion, chaque aiobotocore n'acceptant qu'une plage étroite de botocore ; les correctifs de sécurité arrivent toujours seuls
- `TODO › Transform data` lit Excel avec openpyxl 3.1.5 et xlsxwriter 3.2.9 ; le test de fuite qui garde l'épingle exacte d'openpyxl passe sur eux
- factur-x exige 6.8 hors s390x, la version déjà verrouillée : une régénération ne peut plus retomber sur une 4.x ou 5.x non testée
- Le choix d'interface du déploiement QEMU et de la migration Odoo, et ses préférences dans `TODO › Configuration`, marquent le formulaire TUI d'un 📋 et les questions ligne par ligne d'un 💬
<!-- [en] -->
## Fixed
@ -377,6 +379,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Following a redirect, the cache no longer forwards the client's credentials (Authorization, Cookie, Proxy-Authorization) to another host
- Odoo 18 installs `idna` 3.20 instead of the 3.6 its own requirements pin, which is affected by CVE-2024-3651
- Odoo 18 installs `requests` 2.32.4 instead of the 2.31.0 its own requirements pin, which is affected by CVE-2024-35195 and CVE-2024-47081
- The git mirror of the QEMU cache clones only over `http` and `https`: a client could name an `ssh://` or `git://` repository in its request and make the cache connect, with its service account keys, to a host of its choosing. Repositories fetched over HTTPS are mirrored as before; the binary reports 0.2.17
<!-- [fr] -->
@ -384,6 +387,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- En suivant une redirection, le cache ne transmet plus les identifiants du client (Authorization, Cookie, Proxy-Authorization) à un autre hôte
- Odoo 18 installe `idna` 3.20 au lieu de la 3.6 qu'épinglent ses propres requirements, touchée par CVE-2024-3651
- Odoo 18 installe `requests` 2.32.4 au lieu du 2.31.0 qu'épinglent ses propres requirements, touché par CVE-2024-35195 et CVE-2024-47081
- Le miroir git du cache QEMU ne clone plus qu'en `http` et `https` : un client pouvait nommer un dépôt `ssh://` ou `git://` dans sa requête et faire se connecter le cache, avec les clés de son compte de service, à un hôte de son choix. Les dépôts servis en HTTPS sont mis en miroir comme avant ; le binaire annonce 0.2.17
<!-- [common] -->

View file

@ -98,6 +98,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Dependabot réunit `aiobotocore`, `botocore` et `boto3` dans une seule demande de fusion, chaque aiobotocore n'acceptant qu'une plage étroite de botocore ; les correctifs de sécurité arrivent toujours seuls
- `TODO › Transform data` lit Excel avec openpyxl 3.1.5 et xlsxwriter 3.2.9 ; le test de fuite qui garde l'épingle exacte d'openpyxl passe sur eux
- factur-x exige 6.8 hors s390x, la version déjà verrouillée : une régénération ne peut plus retomber sur une 4.x ou 5.x non testée
- Le choix d'interface du déploiement QEMU et de la migration Odoo, et ses préférences dans `TODO › Configuration`, marquent le formulaire TUI d'un 📋 et les questions ligne par ligne d'un 💬
## Corrigé
@ -178,6 +179,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- En suivant une redirection, le cache ne transmet plus les identifiants du client (Authorization, Cookie, Proxy-Authorization) à un autre hôte
- Odoo 18 installe `idna` 3.20 au lieu de la 3.6 qu'épinglent ses propres requirements, touchée par CVE-2024-3651
- Odoo 18 installe `requests` 2.32.4 au lieu du 2.31.0 qu'épinglent ses propres requirements, touché par CVE-2024-35195 et CVE-2024-47081
- Le miroir git du cache QEMU ne clone plus qu'en `http` et `https` : un client pouvait nommer un dépôt `ssh://` ou `git://` dans sa requête et faire se connecter le cache, avec les clés de son compte de service, à un hôte de son choix. Les dépôts servis en HTTPS sont mis en miroir comme avant ; le binaire annonce 0.2.17
## [1.8.0] - 2026-09-04

View file

@ -98,6 +98,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Dependabot groups `aiobotocore`, `botocore` and `boto3` into one pull request, since each aiobotocore accepts only a narrow botocore range; security fixes still arrive on their own
- `TODO › Transform data` reads Excel with openpyxl 3.1.5 and xlsxwriter 3.2.9; the leak test that guards openpyxl's exact pin passes on them
- factur-x requires 6.8 outside s390x, the version already locked, so a regeneration can no longer fall back to an untested 4.x or 5.x
- The interface chooser of the QEMU deployment and of the Odoo migration, and its preferences in `TODO › Configuration`, mark the TUI form with 📋 and the line by line questions with 💬
## Fixed
@ -178,6 +179,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Following a redirect, the cache no longer forwards the client's credentials (Authorization, Cookie, Proxy-Authorization) to another host
- Odoo 18 installs `idna` 3.20 instead of the 3.6 its own requirements pin, which is affected by CVE-2024-3651
- Odoo 18 installs `requests` 2.32.4 instead of the 2.31.0 its own requirements pin, which is affected by CVE-2024-35195 and CVE-2024-47081
- The git mirror of the QEMU cache clones only over `http` and `https`: a client could name an `ssh://` or `git://` repository in its request and make the cache connect, with its service account keys, to a host of its choosing. Repositories fetched over HTTPS are mirrored as before; the binary reports 0.2.17
## [1.8.0] - 2026-09-04

View file

@ -138,8 +138,14 @@ func (g *GitMirror) backend() string {
// « https://h/o/d.git/info/refs?service=… » rend « https://h/o/d.git » et
// « /info/refs ». Rend faux quand l'URL n'est pas une négociation : le
// découpage n'aurait alors aucun sens.
//
// Rend faux aussi hors de http et https. Le schéma vient de la ligne de
// requête, que le client écrit, et un client git ne passe par un mandataire
// HTTP qu'en HTTP(S) : ssh et git:// ouvrent leur propre connexion. Accepter
// « ssh://hôte/… » ferait cloner le cache vers un hôte choisi par le client,
// avec les clés de son compte de service.
func DepotDeURL(u *url.URL) (string, string, bool) {
if u == nil {
if u == nil || (u.Scheme != "http" && u.Scheme != "https") {
return "", "", false
}
for _, s := range gitSmartPaths {
@ -226,7 +232,10 @@ func (g *GitMirror) Assurer(ctx context.Context, depot string) (string, bool) {
if err := os.MkdirAll(filepath.Dir(chemin), 0o755); err != nil {
return "", false
}
if err := g.git(ctx, "", "clone", "--mirror", depot, chemin); err != nil {
// « -- » : le dépôt ne peut plus se lire comme une option de git.
if err := g.git(
ctx, "", "clone", "--mirror", "--", depot, chemin,
); err != nil {
// Un clonage à moitié fait laisserait un répertoire que la
// prochaine requête prendrait pour un miroir valide.
os.RemoveAll(chemin)
@ -414,10 +423,14 @@ func (g *GitMirror) gitBorne(
// Aucune invite : un dépôt privé doit ÉCHOUER et retomber sur le relais,
// et non bloquer le service en attendant un mot de passe que personne ne
// tapera jamais.
// GIT_ALLOW_PROTOCOL borne les transports de git lui-même, clone comme
// remote update : le miroir ne sert que des négociations HTTP(S), et aucun
// dépôt ne doit l'emmener vers ssh, git:// ou un chemin local.
cmd.Env = append(os.Environ(),
"GIT_TERMINAL_PROMPT=0",
"GIT_ASKPASS=/bin/true",
"GCM_INTERACTIVE=never",
"GIT_ALLOW_PROTOCOL=http:https",
)
sortie, err := cmd.CombinedOutput()
if err != nil {

View file

@ -51,6 +51,49 @@ func TestDepotDeURLRefuse(t *testing.T) {
}
}
// Le schéma vient de la ligne de requête absolue, que le client écrit. Un
// schéma autre que HTTP(S) ferait lancer « git clone » sur un transport que
// jamais un client passant par un mandataire HTTP n'emprunte — ssh, avec les
// clés du compte du service, vers un hôte choisi par le client.
func TestDepotDeURLRefuseLesSchemasNonHTTP(t *testing.T) {
for _, brut := range []string{
"ssh://h/o/d.git/info/refs?service=git-upload-pack",
"git://h/o/d.git/info/refs?service=git-upload-pack",
"file:///srv/o/d.git/info/refs?service=git-upload-pack",
"ext::sh%20-c%20id/info/refs",
} {
u, err := url.Parse(brut)
if err != nil {
continue
}
if depot, _, ok := DepotDeURL(u); ok {
t.Errorf("%s accepté comme dépôt %q", brut, depot)
}
}
// url.Parse ramène le schéma en minuscules : la casse ne refuse rien.
u, _ := url.Parse("HTTP://h/o/d.git/info/refs?service=git-upload-pack")
if _, _, ok := DepotDeURL(u); !ok {
t.Error("HTTP en majuscules refusé")
}
}
// Le dernier rempart est git lui-même : même une URL qui passerait le tri de
// DepotDeURL n'emprunte aucun transport hors HTTP(S). Le dépôt local, que git
// clone sans réseau ni sonde, prouve le refus sans dépendre d'aucun hôte.
func TestGitNEmprunteQueHTTP(t *testing.T) {
nu := depotDEssai(t)
g := &GitMirror{Dir: t.TempDir()}
err := g.git(context.Background(), "", "ls-remote", "--", "file://"+nu)
if err == nil {
t.Fatal("ls-remote file:// accepté")
}
// Le message suit la langue de git ; le transport, lui, y est nommé tel
// quel dans toutes les langues.
if !strings.Contains(err.Error(), "'file'") {
t.Errorf("refus inattendu : %v", err)
}
}
// Le chemin du miroir porte l'HÔTE : deux forges peuvent servir « /odoo/odoo »,
// et les confondre donnerait à l'une le contenu de l'autre.
func TestCheminMiroirSepareLesForges(t *testing.T) {
@ -212,7 +255,9 @@ func TestClonerAuTraversDuMiroir(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
u := &url.URL{Path: r.URL.Path, RawQuery: r.URL.RawQuery}
// L'URL que absoluteURL rend au proxy : schéma et hôte compris.
u := &url.URL{Scheme: "http", Host: r.Host,
Path: r.URL.Path, RawQuery: r.URL.RawQuery}
_, reste, ok := DepotDeURL(u)
if !ok {
http.NotFound(w, r)
@ -413,7 +458,9 @@ func TestCeQueLeMiroirSertEstCompte(t *testing.T) {
var pese int64
srv := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
u := &url.URL{Path: r.URL.Path, RawQuery: r.URL.RawQuery}
// L'URL que absoluteURL rend au proxy : schéma et hôte compris.
u := &url.URL{Scheme: "http", Host: r.Host,
Path: r.URL.Path, RawQuery: r.URL.RawQuery}
_, reste, ok := DepotDeURL(u)
if !ok {
http.NotFound(w, r)

View file

@ -25,7 +25,7 @@ import (
"time"
)
const version = "0.2.16"
const version = "0.2.17"
func main() {
var (

View file

@ -2480,12 +2480,12 @@ TRANSLATIONS = {
"en": "Ask every time",
},
"TUI form": {
"fr": "Formulaire TUI",
"en": "TUI form",
"fr": "📋 Formulaire TUI",
"en": "📋 TUI form",
},
"Classic questions (line by line)": {
"fr": "Questions classiques (ligne par ligne)",
"en": "Classic questions (line by line)",
"fr": "💬 Questions classiques (ligne par ligne)",
"en": "💬 Classic questions (line by line)",
},
"CLI output (easy to copy)": {
"fr": "Sortie CLI (facile à copier)",