Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/web/goimport.go

Line attribution on main · 97 line(s)

d37d301f
Keenan Nemetz
16h ago
1 package web
2
3 // Go module metadata. The go command resolves an import path it does not already
4 // know by fetching https://<import-path>?go-get=1 and reading a
5 // <meta name="go-import"> tag, which names the version control system and the URL
6 // to clone (https://go.dev/ref/mod#vcs-find). Vole publishes a public repository
7 // as <host>/<name>, so `go get rodent.software/vole` finds the repository with
8 // nothing in front of the forge to serve the tag.
9
10 import (
11 "fmt"
12 "html/template"
13 "net/http"
14 "net/url"
15 "strings"
16
17 "rodent.software/vole/forge/access"
18 )
19
ed03b3d1
Keenan Nemetz
3h ago
20 // GoModuleMeta answers the go command's discovery probe. It is tried before the
21 // router (server/forgepaths.go), because a module path is not a page the forge
22 // serves: the first segment names a repository and any segments after it name
23 // packages inside it.
d37d301f
Keenan Nemetz
16h ago
24 //
25 // Only a repository an anonymous reader can read answers. The tag itself is
26 // fetched without credentials — that is the protocol — so a tag for a private
27 // repository would confirm its existence to anyone who guessed the name. Those
28 // return 404 like every other private page, and a private module is fetched by
29 // its Git URL instead. See docs/go-modules.md.
30 //
31 // It reports whether it wrote a response; false falls through to the 404 the
32 // catch-all would have rendered anyway.
ed03b3d1
Keenan Nemetz
3h ago
33 func (w *Web) GoModuleMeta(rw http.ResponseWriter, r *http.Request) bool {
d37d301f
Keenan Nemetz
16h ago
34 if r.URL.Query().Get("go-get") != "1" {
35 return false
36 }
37 name, ok := moduleRepoName(r.URL.Path)
38 if !ok {
39 return false
40 }
41 m, err := w.repo(memoOf(r), name)
42 if err != nil {
43 return false
44 }
45 acl, err := m.ACL()
46 if err != nil || !acl.Can("", access.Read) {
47 return false
48 }
49 // The host comes from the canonical origin, never the request's Host header:
50 // the import prefix decides which module path this instance claims, and a
51 // header an attacker chose must not be able to claim it. The port is not part
52 // of a module path, so only the host name is used.
53 base, err := url.Parse(w.canonicalBase(r))
54 if err != nil || base.Hostname() == "" {
55 return false
56 }
57 prefix := base.Hostname() + "/" + name
58 repoURL := w.cloneURL(r, name)
59
60 rw.Header().Set("Content-Type", "text/html; charset=utf-8")
61 rw.Header().Set("X-Content-Type-Options", "nosniff")
62 fmt.Fprintf(rw, `<!doctype html>
63 <html lang="en">
64 <head>
65 <meta charset="utf-8">
66 <meta name="go-import" content="%s git %s">
67 <title>%s</title>
68 </head>
69 <body>
70 <p>%s is a Go module served from <a href="%s">%s</a>.</p>
71 </body>
72 </html>
73 `,
74 template.HTMLEscapeString(prefix),
75 template.HTMLEscapeString(repoURL),
76 template.HTMLEscapeString(prefix),
77 template.HTMLEscapeString(prefix),
ed03b3d1
Keenan Nemetz
3h ago
78 template.HTMLEscapeString("/"+name),
d37d301f
Keenan Nemetz
16h ago
79 template.HTMLEscapeString(prefix),
80 )
81 return true
82 }
83
84 // moduleRepoName takes the repository from a discovery path. The first segment is
85 // the repository; anything after it is a package inside it, and the same tag
86 // answers for the whole prefix. A path with no segment names nothing.
87 func moduleRepoName(path string) (string, bool) {
88 rest := strings.TrimPrefix(path, "/")
89 if rest == "" {
90 return "", false
91 }
92 name, _, _ := strings.Cut(rest, "/")
93 if name == "" {
94 return "", false
95 }
96 return name, true
97 }