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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 50m ago 4b8c9e1d
history blame raw 3.1 KiB
1package 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
10import (
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 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.
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.
33func (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("/"+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.
87func 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}
98