From b0dbf04be94226cce823cc246623fda5a7e6ea76 Mon Sep 17 00:00:00 2001 From: Jonas Chevalier Date: Sun, 28 Jul 2024 22:32:47 +0200 Subject: [PATCH] feat: allow mapping hashes to subdomains (#48) This is useful for static sites that use absolute paths. If $DOMAIN is set, also map `.$DOMAIN` --- README.md | 1 + api/unpack/index.go | 44 +++++++++++++++++++------------- default.nix | 2 +- go.mod | 1 + go.sum | 2 ++ main.go | 61 +++++++++++++++++++++++++++++++++++++-------- 6 files changed, 82 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index c12239f..09fec29 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ You can use the following environment variables to configure nar-serve: | `PORT` | `8383` | Port number on which nar-service listens | | `HTTP_ADDR` | `:$PORT` | HTTP address to bind the server to. When set, takes precedence over $PORT. | | `NIX_CACHE_URL` | `https://cache.nixos.org` | The URL of the Nix store from which NARs are fetched | +| `DOMAIN` | "" | When set, also serve `.$DOMAIN` paths. | ## Contributing diff --git a/api/unpack/index.go b/api/unpack/index.go index bbf3d95..813c753 100644 --- a/api/unpack/index.go +++ b/api/unpack/index.go @@ -4,6 +4,7 @@ import ( "compress/bzip2" "context" "fmt" + "log" "io" "mime" "net/http" @@ -14,6 +15,7 @@ import ( "github.com/numtide/nar-serve/pkg/nar" "github.com/numtide/nar-serve/pkg/narinfo" + "github.com/go-chi/chi/v5" "github.com/klauspost/compress/zstd" "github.com/ulikunitz/xz" ) @@ -37,34 +39,44 @@ func (h *Handler) MountPath() string { // Handler is the entry-point for @now/go as well as the stub main.go net/http func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - ctx := req.Context() - // remove the mount path from the path - path := strings.TrimPrefix(req.URL.Path, h.mountPath) - // ignore trailing slashes - path = strings.TrimRight(path, "/") - - components := strings.Split(path, "/") - if len(components) == 0 { + narDir := chi.URLParam(req, "narDir") + if narDir == "" { w.Header().Set("Content-Type", "text/plain") http.Error(w, "store path missing", 404) return } - fmt.Println(len(components), components) - narDir := components[0] - narName := strings.Split(narDir, "-")[0] + narHash := strings.Split(narDir, "-")[0] + + h.ServeNAR(narHash, w, req) +} + +func (h *Handler) ServeNAR(narHash string, w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + log.Println("narHash=", narHash) + + // Do some path cleanup + // ignore trailing slashes + newPath := strings.TrimRight(req.URL.Path, "/") + // remove the mount path and nar hash from the path + if strings.HasPrefix(newPath, h.mountPath) { + components := strings.Split(newPath, "/") + newPath = strings.Join(components[4:], "/") + } + newPath = "/" + strings.TrimLeft(newPath, "/") + log.Println("newPath=", newPath) // Get the NAR info to find the NAR - narinfo, err := getNarInfo(ctx, h.cache, narName) + narinfo, err := getNarInfo(ctx, h.cache, narHash) if err != nil { http.Error(w, err.Error(), 500) return } - fmt.Println("narinfo", narinfo) // TODO: consider keeping a LRU cache narPATH := narinfo.URL - fmt.Println("fetching the NAR:", narPATH) + log.Println("fetching the NAR:", narPATH) file, err := h.cache.GetFile(ctx, narPATH) if err != nil { http.Error(w, err.Error(), 500) @@ -104,10 +116,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - newPath := "/" + strings.Join(components[1:], "/") - - fmt.Println("newPath", newPath) - for { hdr, err := narReader.Next() if err != nil { diff --git a/default.nix b/default.nix index 17431c2..0f97c12 100644 --- a/default.nix +++ b/default.nix @@ -7,7 +7,7 @@ rec { pname = "nar-serve"; version = "latest"; src = nixpkgs.lib.cleanSource ./.; - vendorHash = "sha256-hi0KK+TQ3JG6LSMy8wnLDBRnTCwlfwo3ru22sbgX7dc="; + vendorHash = "sha256-td9NYHGYJYPlIj2tnf5I/GnJQOOgODc6TakHFwxyvLQ="; doCheck = false; }; diff --git a/go.mod b/go.mod index ad4e569..85a00e4 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( cloud.google.com/go/storage v1.43.0 github.com/aws/aws-sdk-go v1.55.3 github.com/go-chi/chi/v5 v5.1.0 + github.com/go-chi/hostrouter v0.2.0 github.com/google/go-cmp v0.6.0 github.com/klauspost/compress v1.17.9 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 9f3ff8b..d1fdf0e 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/hostrouter v0.2.0 h1:GwC7TZz8+SlJN/tV/aeJgx4F+mI5+sp+5H1PelQUjHM= +github.com/go-chi/hostrouter v0.2.0/go.mod h1:pJ49vWVmtsKRKZivQx0YMYv4h0aX+Gcn6V23Np9Wf1s= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/main.go b/main.go index 1dc8b84..58a6439 100644 --- a/main.go +++ b/main.go @@ -2,17 +2,20 @@ package main import ( "context" - "log" _ "embed" + "log" "net/http" - "text/template" "os" + "strings" + "text/template" - "github.com/numtide/nar-serve/pkg/libstore" "github.com/numtide/nar-serve/api/unpack" + "github.com/numtide/nar-serve/pkg/libstore" + "github.com/numtide/nar-serve/pkg/nixhash" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/hostrouter" ) //go:embed views/index.html @@ -37,6 +40,7 @@ func main() { port = getEnv("PORT", "8383") addr = getEnv("HTTP_ADDR", "") nixCacheURL = getEnv("NIX_CACHE_URL", getEnv("NAR_CACHE_URL", "https://cache.nixos.org")) + domain = getEnv("DOMAIN", "") ) if addr == "" { @@ -49,29 +53,58 @@ func main() { } // FIXME: get the mountPath from the binary cache /nix-cache-info file - h := unpack.NewHandler(cache, "/nix/store/") + storeHandler := unpack.NewHandler(cache, "/nix/store/") r := chi.NewRouter() - r.Use(middleware.RequestID) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.CleanPath) r.Use(middleware.GetHead) - r.Get("/", func(w http.ResponseWriter, r *http.Request) { + defaultRouter := chi.NewRouter() + defaultRouter.Get("/", func(w http.ResponseWriter, r *http.Request) { data := struct { NixCacheURL string - }{ nixCacheURL } + }{nixCacheURL} if err := indexHTMLTmpl.Execute(w, data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }) - r.Get("/healthz", healthzHandler) - r.Get("/robots.txt", robotsHandler) - r.Method("GET", h.MountPath()+"*", h) + defaultRouter.Get("/healthz", healthzHandler) + defaultRouter.Get("/robots.txt", robotsHandler) + defaultRouter.Method("GET", storeHandler.MountPath()+"{narDir}", storeHandler) + defaultRouter.Method("GET", storeHandler.MountPath()+"{narDir}/*", storeHandler) + + if domain != "" { + narRouter := chi.NewRouter() + narRouter.Get("/*", func(w http.ResponseWriter, r *http.Request) { + // First try to find a nix hash in a subdomain. + narHash := getSubdomain(r.Host) + algo := nixhash.SHA1 + _, err := nixhash.ParseAny(narHash, &algo) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + log.Println("subdomain narHash", narHash) + storeHandler.ServeNAR(narHash, w, r) + }) + + hr := hostrouter.New() + hr.Map("*", defaultRouter) // default + hr.Map("*."+domain, narRouter) + + r.Mount("/", hr) + } else { + r.Mount("/", defaultRouter) + } + + // Front the naked muxer with one that matches sub-domains + log.Println("domain=", domain) log.Println("nixCacheURL=", nixCacheURL) log.Println("addr=", addr) log.Fatal(http.ListenAndServe(addr, r)) @@ -84,3 +117,11 @@ func getEnv(name, def string) string { } return value } + +func getSubdomain(host string) string { + parts := strings.Split(host, ".") + if len(parts) > 1 { + return parts[0] + } + return "" +}