Skip to content

Commit

Permalink
feat: allow mapping hashes to subdomains (#48)
Browse files Browse the repository at this point in the history
This is useful for static sites that use absolute paths.

If $DOMAIN is set, also map `<nar-hash>.$DOMAIN`
  • Loading branch information
zimbatm authored Jul 28, 2024
1 parent 747b599 commit b0dbf04
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 29 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<nar-hash>.$DOMAIN` paths. |

## Contributing

Expand Down
44 changes: 26 additions & 18 deletions api/unpack/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"compress/bzip2"
"context"
"fmt"
"log"
"io"
"mime"
"net/http"
Expand All @@ -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"
)
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ rec {
pname = "nar-serve";
version = "latest";
src = nixpkgs.lib.cleanSource ./.;
vendorHash = "sha256-hi0KK+TQ3JG6LSMy8wnLDBRnTCwlfwo3ru22sbgX7dc=";
vendorHash = "sha256-td9NYHGYJYPlIj2tnf5I/GnJQOOgODc6TakHFwxyvLQ=";
doCheck = false;
};

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
61 changes: 51 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 == "" {
Expand All @@ -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))
Expand All @@ -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 ""
}

0 comments on commit b0dbf04

Please sign in to comment.