This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / spindle / engines / microvm / upload_cache_http.go
3.2 kB 103 lines
1//go:build linux 2 3package microvm 4 5import ( 6 "context" 7 "errors" 8 "io" 9 "log/slog" 10 "net/http" 11 "net/http/httputil" 12 "net/url" 13 "strings" 14) 15 16// httpUploadBackend reverse-proxies guest binary-cache upload traffic to an 17// http(s) upload cache such as ncps. 18type httpUploadBackend struct { 19 handler http.Handler 20} 21 22func newHTTPUploadProxyBackend(target *url.URL, readUpstreams []CacheUpstream, logger *slog.Logger) *httpUploadBackend { 23 return &httpUploadBackend{handler: uploadProxyHandler(target, readUpstreams, logger)} 24} 25 26func (b *httpUploadBackend) ServeHTTP(w http.ResponseWriter, r *http.Request) { 27 b.handler.ServeHTTP(w, r) 28} 29 30func (b *httpUploadBackend) Close() error { return nil } 31 32func uploadProxyHandler(target *url.URL, readUpstreams []CacheUpstream, logger *slog.Logger) http.Handler { 33 rp := httputil.NewSingleHostReverseProxy(target) 34 rp.ErrorLog = slog.NewLogLogger(logger.Handler(), slog.LevelError) 35 36 origDirector := rp.Director 37 rp.Director = func(req *http.Request) { 38 origDirector(req) 39 // ensure host matches target 40 req.Host = target.Host 41 // the transport doesn't turn URL userinfo into basic auth, only 42 // http.Client does, so do it ourselves 43 if user := target.User; user != nil { 44 password, _ := user.Password() 45 req.SetBasicAuth(user.Username(), password) 46 } 47 } 48 49 // before uploading, nix copy asks the destination whether it already has each 50 // path by GET/HEAD-ing <hash>.narinfo and skips the ones it does. we answer 51 // that check across the upload target *and* the read caches: if any of them 52 // already serves the path there is no point uploading it (the guest would 53 // just substitute it from there anyway). 54 narinfoUpstreams := append([]CacheUpstream{{url: target}}, readUpstreams...) 55 exists := newNarinfoExistenceTransport(narinfoUpstreams, logger) 56 57 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 58 if isNarinfoExistenceCheck(r) { 59 serveNarinfoExistence(w, r, exists, logger) 60 return 61 } 62 rp.ServeHTTP(w, r) 63 }) 64} 65 66func newNarinfoExistenceTransport(upstreams []CacheUpstream, logger *slog.Logger) http.RoundTripper { 67 return &parallelRacingTransport{ 68 upstreams: upstreams, 69 underlying: proxyTransport, 70 guardedUnderlying: guardedProxyTransport, 71 logger: logger, 72 } 73} 74 75func isNarinfoExistenceCheck(r *http.Request) bool { 76 if r.Method != http.MethodGet && r.Method != http.MethodHead { 77 return false 78 } 79 return strings.HasSuffix(r.URL.Path, ".narinfo") 80} 81 82func serveNarinfoExistence(w http.ResponseWriter, r *http.Request, exists http.RoundTripper, logger *slog.Logger) { 83 probe := r.Clone(r.Context()) 84 probe.RequestURI = "" 85 86 resp, err := exists.RoundTrip(probe) 87 if err != nil { 88 logger.Warn("upload proxy narinfo check failed, treating as not present", "path", r.URL.Path, "error", err) 89 w.WriteHeader(http.StatusNotFound) 90 return 91 } 92 defer resp.Body.Close() 93 94 for key, values := range resp.Header { 95 for _, value := range values { 96 w.Header().Add(key, value) 97 } 98 } 99 w.WriteHeader(resp.StatusCode) 100 if _, err := io.Copy(w, resp.Body); err != nil && !errors.Is(err, context.Canceled) { 101 logger.Warn("upload proxy narinfo copy failed", "path", r.URL.Path, "error", err) 102 } 103}