diff --git a/script/qemu_cache/gitmirror.go b/script/qemu_cache/gitmirror.go index 96a4f4f..571b3f4 100644 --- a/script/qemu_cache/gitmirror.go +++ b/script/qemu_cache/gitmirror.go @@ -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 { diff --git a/script/qemu_cache/gitmirror_test.go b/script/qemu_cache/gitmirror_test.go index 53d274f..50e6556 100644 --- a/script/qemu_cache/gitmirror_test.go +++ b/script/qemu_cache/gitmirror_test.go @@ -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) diff --git a/script/qemu_cache/main.go b/script/qemu_cache/main.go index 296ece5..a245eb4 100644 --- a/script/qemu_cache/main.go +++ b/script/qemu_cache/main.go @@ -25,7 +25,7 @@ import ( "time" ) -const version = "0.2.16" +const version = "0.2.17" func main() { var (