goget.go

 1package web
 2
 3import (
 4	"net/http"
 5	"net/url"
 6	"path"
 7	"text/template"
 8
 9	"github.com/charmbracelet/soft-serve/server/backend"
10	"github.com/charmbracelet/soft-serve/server/config"
11	"github.com/charmbracelet/soft-serve/server/utils"
12	"github.com/prometheus/client_golang/prometheus"
13	"github.com/prometheus/client_golang/prometheus/promauto"
14	"goji.io/pattern"
15)
16
17var goGetCounter = promauto.NewCounterVec(prometheus.CounterOpts{
18	Namespace: "soft_serve",
19	Subsystem: "http",
20	Name:      "go_get_total",
21	Help:      "The total number of go get requests",
22}, []string{"repo"})
23
24var repoIndexHTMLTpl = template.Must(template.New("index").Parse(`<!DOCTYPE html>
25<html lang="en">
26<head>
27    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
28    <meta http-equiv="refresh" content="0; url=https://godoc.org/{{ .ImportRoot }}/{{.Repo}}">
29    <meta name="go-import" content="{{ .ImportRoot }}/{{ .Repo }} git {{ .Config.HTTP.PublicURL }}/{{ .Repo }}">
30</head>
31<body>
32Redirecting to docs at <a href="https://godoc.org/{{ .ImportRoot }}/{{ .Repo }}">godoc.org/{{ .ImportRoot }}/{{ .Repo }}</a>...
33</body>
34</html>`))
35
36// GoGetHandler handles go get requests.
37type GoGetHandler struct {
38	cfg *config.Config
39	be  *backend.Backend
40}
41
42var _ http.Handler = (*GoGetHandler)(nil)
43
44func (g GoGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
45	repo := pattern.Path(r.Context())
46	repo = utils.SanitizeRepo(repo)
47	be := g.be
48	ctx := r.Context()
49
50	// Handle go get requests.
51	//
52	// Always return a 200 status code, even if the repo doesn't exist.
53	//
54	// https://golang.org/cmd/go/#hdr-Remote_import_paths
55	// https://go.dev/ref/mod#vcs-branch
56	if r.URL.Query().Get("go-get") == "1" {
57		repo := repo
58		importRoot, err := url.Parse(g.cfg.HTTP.PublicURL)
59		if err != nil {
60			http.Error(w, err.Error(), http.StatusInternalServerError)
61			return
62		}
63
64		// find the repo
65		for {
66			if _, err := be.Repository(ctx, repo); err == nil {
67				break
68			}
69
70			if repo == "" || repo == "." || repo == "/" {
71				return
72			}
73
74			repo = path.Dir(repo)
75		}
76
77		if err := repoIndexHTMLTpl.Execute(w, struct {
78			Repo       string
79			Config     *config.Config
80			ImportRoot string
81		}{
82			Repo:       url.PathEscape(repo),
83			Config:     g.cfg,
84			ImportRoot: importRoot.Host,
85		}); err != nil {
86			http.Error(w, err.Error(), http.StatusInternalServerError)
87			return
88		}
89
90		goGetCounter.WithLabelValues(repo).Inc()
91		return
92	}
93
94	http.NotFound(w, r)
95}