[FIX] qemu cache: git mirror clones only over http and https
The scheme of a git negotiation came from the request line the client writes, so "ssh://any-host/x.git/info/refs" made the cache run git clone over ssh, with its service account keys, toward a host the client chose. A git client reaches an HTTP proxy only over HTTP(S), so nothing cached is lost. DepotDeURL now refuses other schemes, the clone ends its options with "--", and GIT_ALLOW_PROTOCOL=http:https bounds git itself. Checked: ssh, git, file and ext refused, a real clone through the mirror still passes, full qemu_cache Go suite green. --- FR --- [FIX] qemu cache : le miroir git ne clone qu'en http et https Le schéma d'une négociation git venait de la ligne de requête écrite par le client : « ssh://hôte-quelconque/x.git/info/refs » faisait lancer au cache un git clone en ssh, avec les clés de son compte de service, vers un hôte choisi par le client. Un client git ne joint un mandataire HTTP qu'en HTTP(S) : rien de ce qui se met en cache n'est perdu. DepotDeURL refuse les autres schémas, le clone clôt ses options par « -- », et GIT_ALLOW_PROTOCOL=http:https borne git lui-même. Vérifié : ssh, git, file et ext refusés, un vrai clone à travers le miroir passe toujours, suite Go de qemu_cache au vert. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
120f92088d
commit
87def09645
3 changed files with 65 additions and 5 deletions
|
|
@ -138,8 +138,14 @@ func (g *GitMirror) backend() string {
|
||||||
// « https://h/o/d.git/info/refs?service=… » rend « https://h/o/d.git » et
|
// « 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
|
// « /info/refs ». Rend faux quand l'URL n'est pas une négociation : le
|
||||||
// découpage n'aurait alors aucun sens.
|
// 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) {
|
func DepotDeURL(u *url.URL) (string, string, bool) {
|
||||||
if u == nil {
|
if u == nil || (u.Scheme != "http" && u.Scheme != "https") {
|
||||||
return "", "", false
|
return "", "", false
|
||||||
}
|
}
|
||||||
for _, s := range gitSmartPaths {
|
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 {
|
if err := os.MkdirAll(filepath.Dir(chemin), 0o755); err != nil {
|
||||||
return "", false
|
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
|
// Un clonage à moitié fait laisserait un répertoire que la
|
||||||
// prochaine requête prendrait pour un miroir valide.
|
// prochaine requête prendrait pour un miroir valide.
|
||||||
os.RemoveAll(chemin)
|
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,
|
// 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
|
// et non bloquer le service en attendant un mot de passe que personne ne
|
||||||
// tapera jamais.
|
// 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(),
|
cmd.Env = append(os.Environ(),
|
||||||
"GIT_TERMINAL_PROMPT=0",
|
"GIT_TERMINAL_PROMPT=0",
|
||||||
"GIT_ASKPASS=/bin/true",
|
"GIT_ASKPASS=/bin/true",
|
||||||
"GCM_INTERACTIVE=never",
|
"GCM_INTERACTIVE=never",
|
||||||
|
"GIT_ALLOW_PROTOCOL=http:https",
|
||||||
)
|
)
|
||||||
sortie, err := cmd.CombinedOutput()
|
sortie, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -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 »,
|
// 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.
|
// et les confondre donnerait à l'une le contenu de l'autre.
|
||||||
func TestCheminMiroirSepareLesForges(t *testing.T) {
|
func TestCheminMiroirSepareLesForges(t *testing.T) {
|
||||||
|
|
@ -212,7 +255,9 @@ func TestClonerAuTraversDuMiroir(t *testing.T) {
|
||||||
|
|
||||||
srv := httptest.NewServer(http.HandlerFunc(
|
srv := httptest.NewServer(http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
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)
|
_, reste, ok := DepotDeURL(u)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
@ -413,7 +458,9 @@ func TestCeQueLeMiroirSertEstCompte(t *testing.T) {
|
||||||
var pese int64
|
var pese int64
|
||||||
srv := httptest.NewServer(http.HandlerFunc(
|
srv := httptest.NewServer(http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
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)
|
_, reste, ok := DepotDeURL(u)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "0.2.16"
|
const version = "0.2.17"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue