Parent directory

goget.go

3420 bytes
  1package web
  2
  3import (
  4	"context"
  5	"net/http"
  6	"net/url"
  7	"path"
  8	"strings"
  9	"text/template"
 10
 11	"charm.land/log/v2"
 12	"github.com/charmbracelet/soft-serve/pkg/access"
 13	"github.com/charmbracelet/soft-serve/pkg/backend"
 14	"github.com/charmbracelet/soft-serve/pkg/config"
 15	"github.com/charmbracelet/soft-serve/pkg/proto"
 16	"github.com/charmbracelet/soft-serve/pkg/utils"
 17	"github.com/gorilla/mux"
 18	"github.com/prometheus/client_golang/prometheus"
 19	"github.com/prometheus/client_golang/prometheus/promauto"
 20)
 21
 22var goGetCounter = promauto.NewCounterVec(prometheus.CounterOpts{
 23	Namespace: "soft_serve",
 24	Subsystem: "http",
 25	Name:      "go_get_total",
 26	Help:      "The total number of go get requests",
 27}, []string{"repo"})
 28
 29var repoIndexHTMLTpl = template.Must(template.New("index").Parse(`<!DOCTYPE html>
 30<html lang="en">
 31<head>
 32    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
 33    <meta http-equiv="refresh" content="0; url=https://godoc.org/{{ .ImportRoot }}/{{.Repo}}">
 34    <meta name="go-import" content="{{ .ImportRoot }}/{{ .Repo }} git {{ .Config.HTTP.PublicURL }}/{{ .Repo }}.git">
 35</head>
 36<body>
 37Redirecting to docs at <a href="https://godoc.org/{{ .ImportRoot }}/{{ .Repo }}">godoc.org/{{ .ImportRoot }}/{{ .Repo }}</a>...
 38</body>
 39</html>
 40`))
 41
 42// GoGetController registers Go import metadata routes.
 43func GoGetController(_ context.Context, r *mux.Router) {
 44	basePrefix := "/{repo:.*}"
 45	r.Handle(basePrefix, withParams(withCaller(http.HandlerFunc(GoGetHandler)))).
 46		Methods(http.MethodGet).
 47		Queries("go-get", "1").
 48		MatcherFunc(func(r *http.Request, _ *mux.RouteMatch) bool {
 49			return !isGitHTTPPath(r.URL.Path)
 50		})
 51}
 52
 53func isGitHTTPPath(path string) bool {
 54	return strings.Contains(path, ".git/info/") ||
 55		strings.Contains(path, ".git/objects/") ||
 56		strings.HasSuffix(path, "/git-upload-archive") ||
 57		strings.HasSuffix(path, "/git-upload-pack") ||
 58		strings.HasSuffix(path, "/git-receive-pack")
 59}
 60
 61// GoGetHandler handles go get requests.
 62func GoGetHandler(w http.ResponseWriter, r *http.Request) {
 63	ctx := r.Context()
 64	cfg := config.FromContext(ctx)
 65	be := backend.FromContext(ctx)
 66	logger := log.FromContext(ctx)
 67	repo := mux.Vars(r)["repo"]
 68
 69	// Handle go get requests.
 70	//
 71	// Walk up the path to the closest repository the caller can read, or
 72	// return a 404 if there is none.
 73	//
 74	// https://golang.org/cmd/go/#hdr-Remote_import_paths
 75	// https://go.dev/ref/mod#vcs-branch
 76	if r.URL.Query().Get("go-get") == "1" {
 77		repo := repo
 78		importRoot, err := url.Parse(cfg.HTTP.PublicURL)
 79		if err != nil {
 80			http.Error(w, err.Error(), http.StatusInternalServerError)
 81			return
 82		}
 83
 84		// Find the closest repository the caller can read; skip the rest.
 85		user := proto.UserFromContext(ctx)
 86		for {
 87			if _, err := be.Repository(ctx, repo); err == nil && be.AccessLevelForUser(ctx, repo, user) >= access.ReadOnlyAccess {
 88				break
 89			}
 90
 91			if repo == "" || repo == "." || repo == "/" {
 92				renderNotFound(w, r)
 93				return
 94			}
 95
 96			repo = path.Dir(repo)
 97		}
 98
 99		if err := repoIndexHTMLTpl.Execute(w, struct {
100			Repo       string
101			Config     *config.Config
102			ImportRoot string
103		}{
104			Repo:       utils.SanitizeRepo(repo),
105			Config:     cfg,
106			ImportRoot: importRoot.Host,
107		}); err != nil {
108			logger.Error("failed to render go get template", "err", err)
109			renderInternalServerError(w, r)
110			return
111		}
112
113		goGetCounter.WithLabelValues(repo).Inc()
114		return
115	}
116
117	renderNotFound(w, r)
118}