Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions internal/strategy/git/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
16 changes: 16 additions & 0 deletions internal/strategy/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"log/slog"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
Expand Down Expand Up @@ -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.
Expand All @@ -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))

Expand Down
49 changes: 5 additions & 44 deletions internal/strategy/git/proxy.go
Original file line number Diff line number Diff line change
@@ -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)
}