feat(gitcli): set receive.maxInputSize on pushes from Config.MaxReceiveBytes
- commit
- 474d85e5f93fd330ba7b40f5194eeb16232ac64f
- author
- brice · · 2026-10-10 08:11 UTC
- parent
- 857b2f4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +52 -20
M internal/adapters/gitcli/backend.go +4 -0
| @@ -33,6 +33,7 @@ func HooksDir(dataDir string) string { return filepath.Join(dataDir, "run", "hoo | |||
| 33 | 33 | // receive.fsckObjects=true | |
| 34 | 34 | // uploadpack.allowFilter=true | |
| 35 | 35 | // http.receivepack=true (push only) | |
| 36 | + | // receive.maxInputSize=<max> (push only, when configured) | |
| 36 | 37 | // | |
| 37 | 38 | // A push also gets REMOTE_USER=owner. | |
| 38 | 39 | func (g *Git) HTTPBackend(ctx context.Context, push bool) (ports.HTTPBackend, error) { | |
| @@ -47,6 +48,9 @@ func (g *Git) HTTPBackend(ctx context.Context, push bool) (ports.HTTPBackend, er | |||
| 47 | 48 | } | |
| 48 | 49 | if push { | |
| 49 | 50 | conf = append(conf, [2]string{"http.receivepack", "true"}) | |
| 51 | + | if g.maxRecv > 0 { | |
| 52 | + | conf = append(conf, [2]string{"receive.maxInputSize", strconv.FormatInt(g.maxRecv, 10)}) | |
| 53 | + | } | |
| 50 | 54 | } | |
| 51 | 55 | env := append([]string(nil), g.env...) | |
| 52 | 56 | env = append(env, | |
| @@ -33,6 +33,7 @@ func HooksDir(dataDir string) string { return filepath.Join(dataDir, "run", "hoo | |||||
| 33 | // receive.fsckObjects=true | 33 | // receive.fsckObjects=true | ||
| 34 | // uploadpack.allowFilter=true | 34 | // uploadpack.allowFilter=true | ||
| 35 | // http.receivepack=true (push only) | 35 | // http.receivepack=true (push only) | ||
| 36 | + | // receive.maxInputSize=<max> (push only, when configured) | |||
| 36 | // | 37 | // | ||
| 37 | // A push also gets REMOTE_USER=owner. | 38 | // A push also gets REMOTE_USER=owner. | ||
| 38 | func (g *Git) HTTPBackend(ctx context.Context, push bool) (ports.HTTPBackend, error) { | 39 | func (g *Git) HTTPBackend(ctx context.Context, push bool) (ports.HTTPBackend, error) { | ||
| @@ -47,6 +48,9 @@ func (g *Git) HTTPBackend(ctx context.Context, push bool) (ports.HTTPBackend, er | |||||
| 47 | } | 48 | } | ||
| 48 | if push { | 49 | if push { | ||
| 49 | conf = append(conf, [2]string{"http.receivepack", "true"}) | 50 | conf = append(conf, [2]string{"http.receivepack", "true"}) | ||
| 51 | + | if g.maxRecv > 0 { | |||
| 52 | + | conf = append(conf, [2]string{"receive.maxInputSize", strconv.FormatInt(g.maxRecv, 10)}) | |||
| 53 | + | } | |||
| 50 | } | 54 | } | ||
| 51 | env := append([]string(nil), g.env...) | 55 | env := append([]string(nil), g.env...) | ||
| 52 | env = append(env, | 56 | env = append(env, | ||
M internal/adapters/gitcli/backend_test.go +43 -19
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "os" | |
| 8 | 8 | "os/exec" | |
| 9 | 9 | "path/filepath" | |
| 10 | + | "strconv" | |
| 10 | 11 | "strings" | |
| 11 | 12 | "testing" | |
| 12 | 13 | ||
| @@ -26,6 +27,41 @@ func envMap(env []string) map[string]string { | |||
| 26 | 27 | return m | |
| 27 | 28 | } | |
| 28 | 29 | ||
| 30 | + | // backendConf returns the GIT_CONFIG_KEY/VALUE pairs of a backend environment | |
| 31 | + | // and checks that GIT_CONFIG_COUNT matches them. | |
| 32 | + | func backendConf(e map[string]string) map[string]string { | |
| 33 | + | conf := map[string]string{} | |
| 34 | + | n := 0 | |
| 35 | + | for { | |
| 36 | + | k, ok := e["GIT_CONFIG_KEY_"+strconv.Itoa(n)] | |
| 37 | + | if !ok { | |
| 38 | + | break | |
| 39 | + | } | |
| 40 | + | conf[k] = e["GIT_CONFIG_VALUE_"+strconv.Itoa(n)] | |
| 41 | + | n++ | |
| 42 | + | } | |
| 43 | + | if e["GIT_CONFIG_COUNT"] != strconv.Itoa(n) { | |
| 44 | + | panic("GIT_CONFIG_COUNT does not match the keys") | |
| 45 | + | } | |
| 46 | + | return conf | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | func TestHTTPBackendReceiveMaxInputSize(t *testing.T) { | |
| 50 | + | ctx := context.Background() | |
| 51 | + | _, data := newGit(t) | |
| 52 | + | bin, err := exec.LookPath("git") | |
| 53 | + | require.NoError(t, err) | |
| 54 | + | g := gitcli.New(gitcli.Config{Bin: bin, DataDir: data, Path: os.Getenv("PATH"), MaxReceiveBytes: 4 << 30}) | |
| 55 | + | ||
| 56 | + | push, err := g.HTTPBackend(ctx, true) | |
| 57 | + | require.NoError(t, err) | |
| 58 | + | assert.Equal(t, "4294967296", backendConf(envMap(push.Env))["receive.maxInputSize"]) | |
| 59 | + | ||
| 60 | + | read, err := g.HTTPBackend(ctx, false) | |
| 61 | + | require.NoError(t, err) | |
| 62 | + | assert.NotContains(t, backendConf(envMap(read.Env)), "receive.maxInputSize", "a fetch does not receive a pack") | |
| 63 | + | } | |
| 64 | + | ||
| 29 | 65 | func TestHTTPBackendEnvironment(t *testing.T) { | |
| 30 | 66 | ctx := context.Background() | |
| 31 | 67 | g, data := newGit(t) | |
| @@ -45,34 +81,22 @@ func TestHTTPBackendEnvironment(t *testing.T) { | |||
| 45 | 81 | assert.Equal(t, "1", e["GIT_HTTP_EXPORT_ALL"]) | |
| 46 | 82 | assert.NotContains(t, e, "REMOTE_USER", "a read is not a push") | |
| 47 | 83 | ||
| 48 | - | conf := map[string]string{} | |
| 49 | - | for i := range 10 { | |
| 50 | - | k, ok := e["GIT_CONFIG_KEY_"+string(rune('0'+i))] | |
| 51 | - | if !ok { | |
| 52 | - | assert.Equal(t, string(rune('0'+i)), e["GIT_CONFIG_COUNT"], "count matches the keys") | |
| 53 | - | break | |
| 54 | - | } | |
| 55 | - | conf[k] = e["GIT_CONFIG_VALUE_"+string(rune('0'+i))] | |
| 56 | - | } | |
| 57 | 84 | assert.Equal(t, map[string]string{ | |
| 58 | 85 | "core.hooksPath": filepath.Join(data, "run", "hooks"), | |
| 59 | 86 | "receive.fsckObjects": "true", | |
| 60 | 87 | "uploadpack.allowFilter": "true", | |
| 61 | - | }, conf) | |
| 88 | + | }, backendConf(e)) | |
| 62 | 89 | ||
| 63 | 90 | push, err := g.HTTPBackend(ctx, true) | |
| 64 | 91 | require.NoError(t, err) | |
| 65 | 92 | pe := envMap(push.Env) | |
| 66 | 93 | assert.Equal(t, "owner", pe["REMOTE_USER"]) | |
| 67 | - | assert.Equal(t, "4", pe["GIT_CONFIG_COUNT"]) | |
| 68 | - | found := false | |
| 69 | - | for i := range 4 { | |
| 70 | - | if pe["GIT_CONFIG_KEY_"+string(rune('0'+i))] == "http.receivepack" { | |
| 71 | - | assert.Equal(t, "true", pe["GIT_CONFIG_VALUE_"+string(rune('0'+i))]) | |
| 72 | - | found = true | |
| 73 | - | } | |
| 74 | - | } | |
| 75 | - | assert.True(t, found, "http.receivepack only for a push") | |
| 94 | + | assert.Equal(t, map[string]string{ | |
| 95 | + | "core.hooksPath": filepath.Join(data, "run", "hooks"), | |
| 96 | + | "receive.fsckObjects": "true", | |
| 97 | + | "uploadpack.allowFilter": "true", | |
| 98 | + | "http.receivepack": "true", | |
| 99 | + | }, backendConf(pe), "http.receivepack only for a push; no size limit unless configured") | |
| 76 | 100 | ||
| 77 | 101 | // Nothing from the caller's environment leaks. | |
| 78 | 102 | t.Setenv("GIT_DIR", "/leak") | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "os" | 7 | "os" | ||
| 8 | "os/exec" | 8 | "os/exec" | ||
| 9 | "path/filepath" | 9 | "path/filepath" | ||
| 10 | + | "strconv" | |||
| 10 | "strings" | 11 | "strings" | ||
| 11 | "testing" | 12 | "testing" | ||
| 12 | 13 | ||||
| @@ -26,6 +27,41 @@ func envMap(env []string) map[string]string { | |||||
| 26 | return m | 27 | return m | ||
| 27 | } | 28 | } | ||
| 28 | 29 | ||||
| 30 | + | // backendConf returns the GIT_CONFIG_KEY/VALUE pairs of a backend environment | |||
| 31 | + | // and checks that GIT_CONFIG_COUNT matches them. | |||
| 32 | + | func backendConf(e map[string]string) map[string]string { | |||
| 33 | + | conf := map[string]string{} | |||
| 34 | + | n := 0 | |||
| 35 | + | for { | |||
| 36 | + | k, ok := e["GIT_CONFIG_KEY_"+strconv.Itoa(n)] | |||
| 37 | + | if !ok { | |||
| 38 | + | break | |||
| 39 | + | } | |||
| 40 | + | conf[k] = e["GIT_CONFIG_VALUE_"+strconv.Itoa(n)] | |||
| 41 | + | n++ | |||
| 42 | + | } | |||
| 43 | + | if e["GIT_CONFIG_COUNT"] != strconv.Itoa(n) { | |||
| 44 | + | panic("GIT_CONFIG_COUNT does not match the keys") | |||
| 45 | + | } | |||
| 46 | + | return conf | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | func TestHTTPBackendReceiveMaxInputSize(t *testing.T) { | |||
| 50 | + | ctx := context.Background() | |||
| 51 | + | _, data := newGit(t) | |||
| 52 | + | bin, err := exec.LookPath("git") | |||
| 53 | + | require.NoError(t, err) | |||
| 54 | + | g := gitcli.New(gitcli.Config{Bin: bin, DataDir: data, Path: os.Getenv("PATH"), MaxReceiveBytes: 4 << 30}) | |||
| 55 | + | ||||
| 56 | + | push, err := g.HTTPBackend(ctx, true) | |||
| 57 | + | require.NoError(t, err) | |||
| 58 | + | assert.Equal(t, "4294967296", backendConf(envMap(push.Env))["receive.maxInputSize"]) | |||
| 59 | + | ||||
| 60 | + | read, err := g.HTTPBackend(ctx, false) | |||
| 61 | + | require.NoError(t, err) | |||
| 62 | + | assert.NotContains(t, backendConf(envMap(read.Env)), "receive.maxInputSize", "a fetch does not receive a pack") | |||
| 63 | + | } | |||
| 64 | + | ||||
| 29 | func TestHTTPBackendEnvironment(t *testing.T) { | 65 | func TestHTTPBackendEnvironment(t *testing.T) { | ||
| 30 | ctx := context.Background() | 66 | ctx := context.Background() | ||
| 31 | g, data := newGit(t) | 67 | g, data := newGit(t) | ||
| @@ -45,34 +81,22 @@ func TestHTTPBackendEnvironment(t *testing.T) { | |||||
| 45 | assert.Equal(t, "1", e["GIT_HTTP_EXPORT_ALL"]) | 81 | assert.Equal(t, "1", e["GIT_HTTP_EXPORT_ALL"]) | ||
| 46 | assert.NotContains(t, e, "REMOTE_USER", "a read is not a push") | 82 | assert.NotContains(t, e, "REMOTE_USER", "a read is not a push") | ||
| 47 | 83 | ||||
| 48 | - | conf := map[string]string{} | |||
| 49 | - | for i := range 10 { | |||
| 50 | - | k, ok := e["GIT_CONFIG_KEY_"+string(rune('0'+i))] | |||
| 51 | - | if !ok { | |||
| 52 | - | assert.Equal(t, string(rune('0'+i)), e["GIT_CONFIG_COUNT"], "count matches the keys") | |||
| 53 | - | break | |||
| 54 | - | } | |||
| 55 | - | conf[k] = e["GIT_CONFIG_VALUE_"+string(rune('0'+i))] | |||
| 56 | - | } | |||
| 57 | assert.Equal(t, map[string]string{ | 84 | assert.Equal(t, map[string]string{ | ||
| 58 | "core.hooksPath": filepath.Join(data, "run", "hooks"), | 85 | "core.hooksPath": filepath.Join(data, "run", "hooks"), | ||
| 59 | "receive.fsckObjects": "true", | 86 | "receive.fsckObjects": "true", | ||
| 60 | "uploadpack.allowFilter": "true", | 87 | "uploadpack.allowFilter": "true", | ||
| 61 | - | }, conf) | 88 | + | }, backendConf(e)) |
| 62 | 89 | ||||
| 63 | push, err := g.HTTPBackend(ctx, true) | 90 | push, err := g.HTTPBackend(ctx, true) | ||
| 64 | require.NoError(t, err) | 91 | require.NoError(t, err) | ||
| 65 | pe := envMap(push.Env) | 92 | pe := envMap(push.Env) | ||
| 66 | assert.Equal(t, "owner", pe["REMOTE_USER"]) | 93 | assert.Equal(t, "owner", pe["REMOTE_USER"]) | ||
| 67 | - | assert.Equal(t, "4", pe["GIT_CONFIG_COUNT"]) | 94 | + | assert.Equal(t, map[string]string{ |
| 68 | - | found := false | 95 | + | "core.hooksPath": filepath.Join(data, "run", "hooks"), |
| 69 | - | for i := range 4 { | 96 | + | "receive.fsckObjects": "true", |
| 70 | - | if pe["GIT_CONFIG_KEY_"+string(rune('0'+i))] == "http.receivepack" { | 97 | + | "uploadpack.allowFilter": "true", |
| 71 | - | assert.Equal(t, "true", pe["GIT_CONFIG_VALUE_"+string(rune('0'+i))]) | 98 | + | "http.receivepack": "true", |
| 72 | - | found = true | 99 | + | }, backendConf(pe), "http.receivepack only for a push; no size limit unless configured") |
| 73 | - | } | |||
| 74 | - | } | |||
| 75 | - | assert.True(t, found, "http.receivepack only for a push") | |||
| 76 | 100 | ||||
| 77 | // Nothing from the caller's environment leaks. | 101 | // Nothing from the caller's environment leaks. | ||
| 78 | t.Setenv("GIT_DIR", "/leak") | 102 | t.Setenv("GIT_DIR", "/leak") | ||
M internal/adapters/gitcli/git.go +5 -1
| @@ -28,6 +28,9 @@ type Config struct { | |||
| 28 | 28 | DataDir string | |
| 29 | 29 | // Path is the PATH given to git, which it uses to find its helpers. | |
| 30 | 30 | Path string | |
| 31 | + | // MaxReceiveBytes is the largest pack a push may send | |
| 32 | + | // (receive.maxInputSize); zero leaves Git unlimited. | |
| 33 | + | MaxReceiveBytes int64 | |
| 31 | 34 | } | |
| 32 | 35 | ||
| 33 | 36 | // Git is the git CLI adapter for ports.GitRepos. | |
| @@ -35,6 +38,7 @@ type Git struct { | |||
| 35 | 38 | bin string | |
| 36 | 39 | dataDir string | |
| 37 | 40 | env []string | |
| 41 | + | maxRecv int64 | |
| 38 | 42 | ||
| 39 | 43 | mu sync.Mutex // guards backend | |
| 40 | 44 | backend string // git-http-backend, resolved on first use | |
| @@ -42,7 +46,7 @@ type Git struct { | |||
| 42 | 46 | ||
| 43 | 47 | // New builds an adapter. It does not run git: call CheckVersion. | |
| 44 | 48 | func New(cfg Config) *Git { | |
| 45 | - | return &Git{bin: cfg.Bin, dataDir: cfg.DataDir, env: buildEnv(cfg.DataDir, cfg.Path)} | |
| 49 | + | return &Git{bin: cfg.Bin, dataDir: cfg.DataDir, env: buildEnv(cfg.DataDir, cfg.Path), maxRecv: cfg.MaxReceiveBytes} | |
| 46 | 50 | } | |
| 47 | 51 | ||
| 48 | 52 | // buildEnv returns the complete environment of every git invocation. Nothing | |
| @@ -28,6 +28,9 @@ type Config struct { | |||||
| 28 | DataDir string | 28 | DataDir string | ||
| 29 | // Path is the PATH given to git, which it uses to find its helpers. | 29 | // Path is the PATH given to git, which it uses to find its helpers. | ||
| 30 | Path string | 30 | Path string | ||
| 31 | + | // MaxReceiveBytes is the largest pack a push may send | |||
| 32 | + | // (receive.maxInputSize); zero leaves Git unlimited. | |||
| 33 | + | MaxReceiveBytes int64 | |||
| 31 | } | 34 | } | ||
| 32 | 35 | ||||
| 33 | // Git is the git CLI adapter for ports.GitRepos. | 36 | // Git is the git CLI adapter for ports.GitRepos. | ||
| @@ -35,6 +38,7 @@ type Git struct { | |||||
| 35 | bin string | 38 | bin string | ||
| 36 | dataDir string | 39 | dataDir string | ||
| 37 | env []string | 40 | env []string | ||
| 41 | + | maxRecv int64 | |||
| 38 | 42 | ||||
| 39 | mu sync.Mutex // guards backend | 43 | mu sync.Mutex // guards backend | ||
| 40 | backend string // git-http-backend, resolved on first use | 44 | backend string // git-http-backend, resolved on first use | ||
| @@ -42,7 +46,7 @@ type Git struct { | |||||
| 42 | 46 | ||||
| 43 | // New builds an adapter. It does not run git: call CheckVersion. | 47 | // New builds an adapter. It does not run git: call CheckVersion. | ||
| 44 | func New(cfg Config) *Git { | 48 | func New(cfg Config) *Git { | ||
| 45 | - | return &Git{bin: cfg.Bin, dataDir: cfg.DataDir, env: buildEnv(cfg.DataDir, cfg.Path)} | 49 | + | return &Git{bin: cfg.Bin, dataDir: cfg.DataDir, env: buildEnv(cfg.DataDir, cfg.Path), maxRecv: cfg.MaxReceiveBytes} |
| 46 | } | 50 | } | ||
| 47 | 51 | ||||
| 48 | // buildEnv returns the complete environment of every git invocation. Nothing | 52 | // buildEnv returns the complete environment of every git invocation. Nothing | ||