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}