diff --git a/internal/strategy/git/backend.go b/internal/strategy/git/backend.go index dff362f..859a729 100644 --- a/internal/strategy/git/backend.go +++ b/internal/strategy/git/backend.go @@ -69,7 +69,13 @@ func (s *Strategy) executeClone(ctx context.Context, c *clone) error { } // #nosec G204 - c.upstreamURL and c.path are controlled by us - cmd := exec.CommandContext(ctx, "git", "clone", "--bare", "--mirror", c.upstreamURL, c.path) + // Configure git for large repositories to avoid network buffer issues + cmd := exec.CommandContext(ctx, "git", "clone", + "--bare", "--mirror", + "-c", "http.postBuffer=524288000", // 500MB buffer + "-c", "http.lowSpeedLimit=1000", // 1KB/s minimum speed + "-c", "http.lowSpeedTime=600", // 10 minute timeout at low speed + c.upstreamURL, c.path) output, err := cmd.CombinedOutput() if err != nil { logger.ErrorContext(ctx, "git clone failed", @@ -87,7 +93,12 @@ func (s *Strategy) executeFetch(ctx context.Context, c *clone) error { logger := logging.FromContext(ctx) // #nosec G204 - c.path is controlled by us - cmd := exec.CommandContext(ctx, "git", "-C", c.path, "fetch", "--all") + // Configure git for large repositories to avoid network buffer issues + cmd := exec.CommandContext(ctx, "git", "-C", c.path, + "-c", "http.postBuffer=524288000", // 500MB buffer + "-c", "http.lowSpeedLimit=1000", // 1KB/s minimum speed + "-c", "http.lowSpeedTime=600", // 10 minute timeout at low speed + "fetch", "--all") output, err := cmd.CombinedOutput() if err != nil { logger.ErrorContext(ctx, "git fetch failed", diff --git a/internal/strategy/git/git.go b/internal/strategy/git/git.go index 22179b4..7b7149b 100644 --- a/internal/strategy/git/git.go +++ b/internal/strategy/git/git.go @@ -5,6 +5,7 @@ import ( "context" "log/slog" "net/http" + "net/http/httputil" "net/url" "os" "path/filepath" @@ -54,6 +55,7 @@ type Strategy struct { clones map[string]*clone clonesMu sync.RWMutex httpClient *http.Client + proxy *httputil.ReverseProxy } // New creates a new Git caching strategy. @@ -79,6 +81,20 @@ func New(ctx context.Context, config Config, cache cache.Cache, mux strategy.Mux httpClient: http.DefaultClient, } + s.proxy = &httputil.ReverseProxy{ + Director: func(req *http.Request) { + req.URL.Scheme = "https" + req.URL.Host = req.PathValue("host") + req.URL.Path = "/" + req.PathValue("path") + req.Host = req.URL.Host + }, + Transport: s.httpClient.Transport, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + logging.FromContext(r.Context()).ErrorContext(r.Context(), "Upstream request failed", slog.String("error", err.Error())) + w.WriteHeader(http.StatusBadGateway) + }, + } + mux.Handle("GET /git/{host}/{path...}", http.HandlerFunc(s.handleRequest)) mux.Handle("POST /git/{host}/{path...}", http.HandlerFunc(s.handleRequest)) diff --git a/internal/strategy/git/proxy.go b/internal/strategy/git/proxy.go index 4a3e5aa..2db0b96 100644 --- a/internal/strategy/git/proxy.go +++ b/internal/strategy/git/proxy.go @@ -1,59 +1,20 @@ package git import ( - "io" "log/slog" "net/http" - "github.com/block/cachew/internal/httputil" "github.com/block/cachew/internal/logging" ) // forwardToUpstream forwards a request to the upstream Git server. func (s *Strategy) forwardToUpstream(w http.ResponseWriter, r *http.Request, host, pathValue string) { - ctx := r.Context() - logger := logging.FromContext(ctx) + logger := logging.FromContext(r.Context()) - upstreamURL := "https://" + host + "/" + pathValue - if r.URL.RawQuery != "" { - upstreamURL += "?" + r.URL.RawQuery - } - - logger.DebugContext(ctx, "Forwarding to upstream", + logger.DebugContext(r.Context(), "Forwarding to upstream", slog.String("method", r.Method), - slog.String("upstream_url", upstreamURL)) - - upstreamReq, err := http.NewRequestWithContext(ctx, r.Method, upstreamURL, r.Body) - if err != nil { - httputil.ErrorResponse(w, r, http.StatusInternalServerError, "failed to create upstream request") - return - } - - // Copy relevant headers - for _, header := range []string{"Content-Type", "Content-Length", "Content-Encoding", "Accept", "Accept-Encoding", "Git-Protocol"} { - if v := r.Header.Get(header); v != "" { - upstreamReq.Header.Set(header, v) - } - } - - resp, err := s.httpClient.Do(upstreamReq) - if err != nil { - logger.ErrorContext(ctx, "Upstream request failed", slog.String("error", err.Error())) - httputil.ErrorResponse(w, r, http.StatusBadGateway, "upstream request failed") - return - } - defer resp.Body.Close() - - // Copy response headers - for key, values := range resp.Header { - for _, value := range values { - w.Header().Add(key, value) - } - } - - w.WriteHeader(resp.StatusCode) + slog.String("host", host), + slog.String("path", pathValue)) - if _, err := io.Copy(w, resp.Body); err != nil { - logger.ErrorContext(ctx, "Failed to stream upstream response", slog.String("error", err.Error())) - } + s.proxy.ServeHTTP(w, r) }