This repository has no description
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 ¶llelRacingTransport{
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}