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