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 · 1h ago 4b8c9e1d
history blame raw 12.9 KiB
1package server
2
3// The HTTP route table, grouped by the surface each route belongs to.
4//
5// A repository owns the root of the URL space: `/<repository>/issues/12` is a
6// page, `/<repository>.git` is its Git transport. The forge's own pages live
7// under `/-/`, a prefix no repository name can start with, so the two spaces can
8// never collide and no route has to know about repository names.
9//
10// The table is split into one function per area rather than written in one list
11// because Go's ServeMux ranks patterns by specificity, not by registration
12// order: when two patterns overlap (`/{name}/releases/{tag...}` and
13// `/{name}/releases/{tag}/archive.tar`, say) they have to be read together to
14// know which one wins. Grouping keeps a route next to the routes it competes
15// with.
16
17import (
18 "fmt"
19 "net/http"
20)
21
22// routes builds the forge's HTTP surface. The catch-all is registered last for
23// the benefit of the reader; the router itself does not care where it appears.
24func (s *Server) routes() *http.ServeMux {
25 mux := http.NewServeMux()
26 s.assetRoutes(mux)
27 s.globalRoutes(mux)
28 s.authRoutes(mux)
29 s.adminRoutes(mux)
30 s.repoRoutes(mux)
31 // Unknown paths get the same styled 404 as a missing or private repository.
32 mux.HandleFunc("/{path...}", s.Web.NotFoundPage)
33 return mux
34}
35
36// assetRoutes serves the embedded stylesheet, scripts, and favicon. Assets are
37// compiled into the binary, so there is no directory to deploy beside it.
38func (s *Server) assetRoutes(mux *http.ServeMux) {
39 mux.Handle("GET /-/static/", s.Web.Static())
40}
41
42// globalRoutes are the destinations that are not scoped to a repository:
43// the repository list, the instance feed, the redirects to the Admin page, and the
44// probes a reverse proxy or a monitor calls.
45func (s *Server) globalRoutes(mux *http.ServeMux) {
46 w := s.Web
47 mux.HandleFunc("GET /{$}", w.IndexPage)
48 mux.HandleFunc("GET /-/activity", w.InstanceActivity)
49 // Crawlers discover the public repositories and pages through the sitemap.
50 // robots.txt names the paths worth fetching and points at the index; the
51 // per-repository files are below /{name}, where their URLs belong. The two
52 // crawler files stay at the root because a crawler only looks there, which
53 // also makes those three names unavailable to a repository.
54 mux.HandleFunc("GET /robots.txt", w.Robots)
55 mux.HandleFunc("GET /sitemap.xml", w.SitemapIndex)
56 mux.HandleFunc("GET /sitemap-pages.xml", w.SitemapPages)
57 // Accounts are public on this instance: the directory lists everybody, and a
58 // profile answers for any name in it. Their activity is not public — every
59 // count on those pages follows the same read rule as the repository it came
60 // from — so these are ordinary global reads, not an admin surface. The
61 // directory answers both spellings so `/-/u` does not redirect to `/-/u/`.
62 mux.HandleFunc("GET /-/u", w.PeoplePage)
63 mux.HandleFunc("GET /-/u/{$}", w.PeoplePage)
64 mux.HandleFunc("GET /-/u/{name}", w.ProfilePage)
65 mux.HandleFunc("GET /-/search/palette", w.SearchPalette)
66 // The status tabs moved onto the Admin page; old links redirect to the tab they
67 // named so a bookmark lands where it used to.
68 mux.HandleFunc("GET /-/status", w.StatusRedirect)
69 mux.HandleFunc("GET /-/healthz", func(rw http.ResponseWriter, r *http.Request) {
70 rw.Header().Set("Content-Type", "text/plain")
71 fmt.Fprint(rw, "ok\n")
72 })
73 // Runner presence moved to the Admin page's Runners tab; the old URL is
74 // linked from notes and scripts, so it redirects rather than 404s.
75 mux.HandleFunc("GET /-/runners", func(rw http.ResponseWriter, r *http.Request) {
76 http.Redirect(rw, r, "/-/admin?tab=runners", http.StatusMovedPermanently)
77 })
78}
79
80// authRoutes cover the passkey ceremonies, the session, and the account page.
81// The JSON ceremony endpoints answer only POST because the browser posts the
82// credential there; the pages they belong to are GETs.
83func (s *Server) authRoutes(mux *http.ServeMux) {
84 w := s.Web
85 mux.HandleFunc("GET /-/login", w.LoginPage)
86 mux.HandleFunc("GET /-/dev/login", w.DevLogin)
87 mux.HandleFunc("GET /-/register", w.RegisterPage)
88 mux.HandleFunc("POST /-/auth/register/begin", w.RegisterBegin)
89 mux.HandleFunc("POST /-/auth/register/finish", w.RegisterFinish)
90 mux.HandleFunc("POST /-/auth/login/begin", w.LoginBegin)
91 mux.HandleFunc("POST /-/auth/login/finish", w.LoginFinish)
92 mux.HandleFunc("POST /-/logout", w.Logout)
93 mux.HandleFunc("GET /-/invite", w.InvitePage)
94 // Enrollment links used to carry the token in the path. They still work, but the
95 // token is moved into the fragment on the way, where no log can see it.
96 mux.HandleFunc("GET /-/invite/{token}", w.LegacyInvitePage)
97 mux.HandleFunc("POST /-/auth/invite/begin", w.InviteBegin)
98 mux.HandleFunc("POST /-/auth/invite/finish", w.InviteFinish)
99 mux.HandleFunc("GET /-/account", w.AccountPage)
100 mux.HandleFunc("POST /-/account/keys", w.AddAccountKey)
101 mux.HandleFunc("POST /-/account/keys/remove", w.RemoveAccountKey)
102 mux.HandleFunc("POST /-/account/passkeys/rename", w.RenameAccountPasskey)
103 mux.HandleFunc("POST /-/account/passkeys/remove", w.RemoveAccountPasskey)
104 mux.HandleFunc("POST /-/account/passkeys/begin", w.AddPasskeyBegin)
105 mux.HandleFunc("POST /-/account/passkeys/finish", w.AddPasskeyFinish)
106}
107
108// adminRoutes are the instance-wide pages. Every handler checks the instance
109// admin role itself; hiding a link is presentation, never authorization.
110func (s *Server) adminRoutes(mux *http.ServeMux) {
111 w := s.Web
112 mux.HandleFunc("GET /-/admin", w.AdminPage)
113 mux.HandleFunc("POST /-/admin/instance/repo", w.CreateInstanceRepo)
114 mux.HandleFunc("GET /-/repos/new", w.AdminNewRepoPage)
115 mux.HandleFunc("POST /-/repos", w.CreateAdminRepo)
116 // The roster is the Accounts tab now; /users stays as the URL the account
117 // detail pages are still below.
118 mux.HandleFunc("GET /-/users", w.AdminUsersPage)
119 mux.HandleFunc("POST /-/users", w.CreateAdminUser)
120 mux.HandleFunc("POST /-/users/bots", w.CreateAdminBot)
121 mux.HandleFunc("GET /-/users/{name}", w.AdminUserPage)
122 mux.HandleFunc("POST /-/users/{name}/role", w.SetAdminUserRole)
123 mux.HandleFunc("POST /-/users/{name}/retire", w.RetireAdminUser)
124 mux.HandleFunc("POST /-/users/{name}/invite", w.InviteAdminUser)
125 mux.HandleFunc("POST /-/users/{name}/sessions/revoke", w.RevokeAdminUserSessions)
126 mux.HandleFunc("POST /-/users/{name}/keys/remove", w.RemoveAdminUserKey)
127}
128
129// repoRoutes are every page and write below /{name}. A private repository
130// answers 404 here exactly as a missing one does; the handlers share that rule
131// through web.open. These patterns overlap the forge's `/-/` routes only in
132// shape; the literal `-` prefix is more specific, so it always wins.
133func (s *Server) repoRoutes(mux *http.ServeMux) {
134 w := s.Web
135
136 // Repository summary and feed, and the header's open counts. The tabs poll
137 // the counts while the page is visible.
138 mux.HandleFunc("GET /{name}", w.Overview)
139 mux.HandleFunc("GET /{name}/counts", w.RepoCounts)
140 mux.HandleFunc("GET /{name}/activity", w.Activity)
141 mux.HandleFunc("GET /{name}/sitemap.xml", w.RepoSitemap)
142
143 // Code browsing: one route per view of a ref, with the path as a wildcard
144 // because a branch name may contain "/" and so may a file path.
145 mux.HandleFunc("GET /{name}/code", w.Code)
146 mux.HandleFunc("GET /{name}/search", w.CodeSearch)
147 mux.HandleFunc("GET /{name}/compare", w.Compare)
148 mux.HandleFunc("GET /{name}/compare/{range...}", w.Compare)
149 mux.HandleFunc("GET /{name}/tree/{ref}", w.Tree)
150 mux.HandleFunc("GET /{name}/tree/{ref}/{path...}", w.Tree)
151 mux.HandleFunc("GET /{name}/blob/{ref}/{path...}", w.Blob)
152 mux.HandleFunc("GET /{name}/raw/{ref}/{path...}", w.Raw)
153 mux.HandleFunc("GET /{name}/commits/{ref}", w.Commits)
154 mux.HandleFunc("GET /{name}/commits/{ref}/{path...}", w.FileHistory)
155 mux.HandleFunc("GET /{name}/blame/{ref}/{path...}", w.Blame)
156 mux.HandleFunc("GET /{name}/commit/{sha}", w.CommitView)
157 mux.HandleFunc("GET /{name}/commit/{sha}/{path...}", w.CommitView)
158
159 // Issues: the list and detail pages, then the writes that replace a section
160 // of the detail page rather than navigating.
161 mux.HandleFunc("GET /{name}/issues", w.Issues)
162 mux.HandleFunc("GET /{name}/issues/new", w.NewIssuePage)
163 mux.HandleFunc("POST /{name}/issues", w.CreateIssue)
164 mux.HandleFunc("GET /{name}/issues/{id}", w.IssueView)
165 mux.HandleFunc("GET /{name}/issues/{id}/edit", w.IssueEditPage)
166 mux.HandleFunc("POST /{name}/issues/{id}/edit", w.UpdateIssueDetails)
167 // One alias route for both kinds. `#42` in a body may name an issue or a
168 // change request, and the index is what knows which.
169 mux.HandleFunc("GET /{name}/n/{num}", w.NumberAlias)
170 mux.HandleFunc("POST /{name}/issues/{id}/comments", w.AddIssueComment)
171 mux.HandleFunc("POST /{name}/issues/{id}/comments/{comment}/edit", w.EditIssueComment)
172 mux.HandleFunc("POST /{name}/issues/{id}/comments/{comment}/delete", w.DeleteIssueComment)
173 mux.HandleFunc("POST /{name}/issues/{id}/state", w.ChangeIssueState)
174 mux.HandleFunc("POST /{name}/issues/{id}/labels", w.SetIssueLabels)
175 mux.HandleFunc("POST /{name}/issues/{id}/assignees", w.SetIssueAssignees)
176
177 // Change requests: conversation, the Files tab, the Runs tab, and the
178 // approval and comment writes.
179 mux.HandleFunc("GET /{name}/changes", w.Requests)
180 mux.HandleFunc("GET /{name}/changes/new", w.NewChangePage)
181 mux.HandleFunc("POST /{name}/changes", w.CreateChangeRequest)
182 mux.HandleFunc("GET /{name}/changes/{id}", w.RequestView)
183 mux.HandleFunc("GET /{name}/changes/{id}/edit", w.RequestEditPage)
184 mux.HandleFunc("POST /{name}/changes/{id}/edit", w.UpdateRequestDetails)
185 mux.HandleFunc("GET /{name}/changes/{id}/files", w.RequestFiles)
186 mux.HandleFunc("GET /{name}/changes/{id}/files/{path...}", w.RequestFiles)
187 mux.HandleFunc("GET /{name}/changes/{id}/runs", w.RequestRuns)
188 mux.HandleFunc("GET /{name}/changes/{id}/live", w.RequestLive)
189 mux.HandleFunc("GET /{name}/changes/{id}/issues/search", w.SearchRequestIssues)
190 mux.HandleFunc("GET /{name}/changes/{id}/comment-form", w.RequestCommentForm)
191 mux.HandleFunc("POST /{name}/changes/{id}/approve", w.ApproveRequest)
192 mux.HandleFunc("POST /{name}/changes/{id}/merge", w.MergeRequest)
193 mux.HandleFunc("POST /{name}/changes/{id}/merge/cancel", w.CancelMerge)
194 mux.HandleFunc("POST /{name}/changes/{id}/comments", w.AddRequestComment)
195 mux.HandleFunc("POST /{name}/changes/{id}/comments/{comment}/edit", w.EditRequestComment)
196 mux.HandleFunc("POST /{name}/changes/{id}/comments/{comment}/delete", w.DeleteRequestComment)
197 mux.HandleFunc("POST /{name}/changes/{id}/state", w.ChangeRequestState)
198 mux.HandleFunc("POST /{name}/changes/{id}/target", w.UpdateRequestTarget)
199 mux.HandleFunc("POST /{name}/changes/{id}/issues", w.UpdateRequestIssues)
200 mux.HandleFunc("POST /{name}/ci/allow", w.AllowContributorCI)
201
202 // Releases. A tag may contain "/", so the detail route takes the rest of the
203 // path; the archive routes are more specific and win for their suffixes.
204 mux.HandleFunc("GET /{name}/releases", w.Releases)
205 mux.HandleFunc("GET /{name}/releases/new", w.ReleaseNewPage)
206 mux.HandleFunc("POST /{name}/releases", w.CreateRelease)
207 mux.HandleFunc("GET /{name}/releases/edit", w.ReleaseEditPage)
208 mux.HandleFunc("POST /{name}/releases/edit", w.UpdateRelease)
209 mux.HandleFunc("GET /{name}/releases/{tag...}", w.ReleaseView)
210 mux.HandleFunc("GET /{name}/releases/{tag}/archive.tar", w.ReleaseTar)
211 mux.HandleFunc("GET /{name}/releases/{tag}/archive.tar.gz", w.ReleaseTarGz)
212 mux.HandleFunc("GET /{name}/releases/{tag}/artifacts/{artifact}", w.ReleaseArtifact)
213
214 // CI: the run list and one run with its log and artifacts. Cancel and rerun
215 // are the only writes.
216 mux.HandleFunc("GET /{name}/runs", w.RunsPage)
217 mux.HandleFunc("GET /{name}/workflows/dispatch", w.WorkflowDispatchPage)
218 mux.HandleFunc("POST /{name}/workflows/dispatch", w.WorkflowDispatch)
219 mux.HandleFunc("GET /{name}/runs/{id}", w.RunPage)
220 mux.HandleFunc("POST /{name}/runs/{id}/cancel", w.CancelRun)
221 mux.HandleFunc("POST /{name}/runs/{id}/rerun", w.Rerun)
222 mux.HandleFunc("GET /{name}/runs/{id}/log", w.RunLog)
223 mux.HandleFunc("GET /{name}/runs/{id}/artifacts/{artifact}", w.RunArtifact)
224
225 // Locks and settings. Settings carries both the repository's own settings
226 // and, for an instance admin, the access rules.
227 mux.HandleFunc("GET /{name}/locks", w.LocksPage)
228 mux.HandleFunc("GET /{name}/settings", w.SettingsPage)
229 mux.HandleFunc("POST /{name}/settings", w.SaveSettings)
230 mux.HandleFunc("POST /{name}/settings/secrets", w.SaveSecret)
231 mux.HandleFunc("POST /{name}/settings/secrets/delete", w.DeleteSecret)
232 mux.HandleFunc("POST /{name}/settings/access", w.SaveRepositoryAccess)
233 mux.HandleFunc("POST /{name}/settings/access/add", w.AddRepositoryAccess)
234 mux.HandleFunc("POST /{name}/settings/rename", w.RenameRepository)
235 mux.HandleFunc("POST /{name}/settings/delete", w.DeleteRepository)
236
237 // Git LFS reads share the same read rule as clone. Uploads are refused here
238 // and happen over SSH. The transport itself is not a route: <name>.git/… is
239 // recognized before the mux (server/gittransport.go).
240 mux.HandleFunc("POST /{name}/info/lfs/objects/batch", s.LFSBatch)
241 mux.HandleFunc("GET /{name}/info/lfs/objects/{oid}", s.LFSObject)
242 mux.HandleFunc("HEAD /{name}/info/lfs/objects/{oid}", s.LFSObject)
243}
244