vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Bound the anonymous Git HTTP read path
86991e7020a418a08a5e3a36ce8f296561392e3b · Keenan Nemetz · 13d ago · parent e05f1c48
Bound the anonymous Git HTTP read path The transport that needs no account had no bound on anything: the request body was capped only at "not negative", the backend ran without a request context or deadline, the path check accepted any suffix after `git-upload-pack` and handed it to the CGI as `PATH_INFO`, and the server set no `ReadTimeout` and no `MaxHeaderBytes`. Bound what the read side can spend, which is every dimension available to a fetch and none of the ones a push would need: a 2 MiB body cap with a required `Content-Length`, exactly two endpoints with one method and one service each so the CGI never sees an unvalidated path, `exec.CommandContext` plus `WaitDelay` plus an explicit kill so a disconnect or an expired budget stops git instead of leaving it running, and a concurrency semaphore with a per-client bound that answers 503 with Retry-After rather than queueing. The client address is the connection's own unless the operator names a proxy header, because behind a proxy the connection address is the proxy. Both limits and the header name are configuration, `git_http` turns the transport off entirely, and `status` lists them so an operator can see what is in force. `WriteTimeout` is deliberately not set: clone, archives, LFS objects and site files are streamed, so those flows are bounded where they are produced.
server/server.go +20−4
| @@ -76,9 +76,13 @@ func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error) | |||
| 76 | 76 | } | |
| 77 | 77 | } | |
| 78 | 78 | proxy := githttp.New(githttp.Config{ | |
| 79 | Root: cfg.ReposDir(), | ||
| 80 | Open: inst.Repos.Open, | ||
| 81 | Log: log, | ||
| 79 | Root: cfg.ReposDir(), | ||
| 80 | Open: inst.Repos.Open, | ||
| 81 | Log: log, | ||
| 82 | MaxConcurrent: cfg.GitHTTPMaxConcurrent, | ||
| 83 | MaxPerClient: cfg.GitHTTPMaxPerClient, | ||
| 84 | ClientIPHeader: cfg.GitHTTPClientIPHeader, | ||
| 85 | RequestTimeout: time.Duration(cfg.GitHTTPTimeoutSeconds) * time.Second, | ||
| 82 | 86 | }) | |
| 83 | 87 | ssh, err := ssh.New(ssh.Config{ | |
| 84 | 88 | Identity: inst.Identity, | |
| @@ -190,7 +194,17 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 190 | 194 | httpSrv := &http.Server{ | |
| 191 | 195 | Handler: s.Handler(), | |
| 192 | 196 | ReadHeaderTimeout: 15 * time.Second, | |
| 193 | IdleTimeout: 120 * time.Second, | ||
| 197 | // ReadTimeout bounds the request, body included. The only bodies on the | ||
| 198 | // HTTP surface are a login form, a comment and a fetch negotiation, all of | ||
| 199 | // which are small and arrive long before this. | ||
| 200 | ReadTimeout: 60 * time.Second, | ||
| 201 | // WriteTimeout is deliberately unset: a clone, a release tarball, an LFS | ||
| 202 | // object and a site file are streamed, and a wall-clock limit on writing the | ||
| 203 | // response would truncate a legitimate large download. Those streams are | ||
| 204 | // bounded where they are produced instead — the git transport by its own | ||
| 205 | // request deadline, archives by a concurrency bound, downloads by a size cap. | ||
| 206 | MaxHeaderBytes: 64 << 10, | ||
| 207 | IdleTimeout: 120 * time.Second, | ||
| 194 | 208 | } | |
| 195 | 209 | var httpsLn net.Listener | |
| 196 | 210 | var httpsSrv *http.Server | |
| @@ -206,6 +220,8 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 206 | 220 | Handler: s.Handler(), | |
| 207 | 221 | TLSConfig: s.acme.tlsConfig(), | |
| 208 | 222 | ReadHeaderTimeout: 15 * time.Second, | |
| 223 | ReadTimeout: 60 * time.Second, | ||
| 224 | MaxHeaderBytes: 64 << 10, | ||
| 209 | 225 | IdleTimeout: 120 * time.Second, | |
| 210 | 226 | } | |
| 211 | 227 | } | |