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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

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

Publish repositories as Go modules and move the module path to rodent.software

d37d301fd4881ee37855387377db5a7aed843017 · Keenan Nemetz · 21h ago · parent 1c363d53

Publish repositories as Go modules and move the module path to rodent.software

The module named a GitHub repository that does not exist, and a module path
under the forge could not be fetched anyway: the go command resolves an import
path by asking it for a go-import tag first, and the forge served only pages.

Answer the <host>/<repository> discovery probe from the catch-all route with the
repository's tag, naming its Git transport and that prefix, so a public
repository is importable with no vanity service in front of the instance. Only
a repository an anonymous reader can read answers: the tag is fetched without
credentials, so a tag for a private repository would confirm its existence to
anyone who guessed the name. The prefix host comes from public_url, never the
request's Host header, for the same reason the clone URL does.

With the path resolvable, rename the module to rodent.software/vole: imports,
the build line in the getting-started guide, and the packaged units and
metadata follow it. docs/go-modules.md explains the lookup, what the instance
must have, and why a repository named after a route cannot claim the prefix.

server/web/goimport.go +97

@@ -0,0 +1,97 @@
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
20 // goModuleMeta answers the go command's discovery probe. It is tried from the
21 // catch-all route, because a module path is not a page the forge serves: the
22 // first segment names a repository and any segments after it name packages
23 // inside it.
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.
33 func (w *Web) goModuleMeta(rw http.ResponseWriter, r *http.Request) bool {
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),
78 template.HTMLEscapeString("/r/"+name),
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 }