| 1 |
|
package web |
| 2 |
|
|
| 3 |
|
import ( |
| 4 |
|
"net/http" |
| 5 |
|
"strings" |
| 6 |
|
|
| 7 |
|
"github.com/go-chi/chi/v5" |
| 8 |
|
"sourcecraft.dev/bigbes/sr-ht-ecore/assets" |
| 9 |
|
"sourcecraft.dev/bigbes/sr-ht-ecore/chimw" |
| 10 |
|
"sourcecraft.dev/bigbes/sr-ht-ecore/csrf" |
| 11 |
|
"sourcecraft.dev/bigbes/sr-ht-ecore/middleware" |
| 12 |
|
) |
| 13 |
|
|
| 14 |
|
// Register mounts every diff.sr.ht route onto r, inside a group carrying the |
| 15 |
|
// three middlewares that need this Server. The chain documented on the package |
| 16 |
|
// (config + authz at a minimum) is still the caller's, and stays outside this |
| 17 |
|
// group. |
| 18 |
|
// |
| 19 |
|
// - PrivateCache, because every page here is a page to its owner and a 404 to |
| 20 |
|
// everybody else, at a URL that says nothing about the viewer. The static |
| 21 |
|
// route opts back out per asset, from inside assets.Handler, and only once |
| 22 |
|
// the bytes are committed. |
| 23 |
|
// - RecoverPanics, so a panicking handler produces this service's own error |
| 24 |
|
// page instead of a dropped connection — and, when the response has already |
| 25 |
|
// started, a dropped connection instead of an error page appended to half a |
| 26 |
|
// rendered diff. |
| 27 |
|
// - csrf.Require, which guards a future rather than a present: every route |
| 28 |
|
// below is a GET, so nothing is refused by it today. It is installed anyway |
| 29 |
|
// because the day somebody adds the first POST is exactly the day nobody |
| 30 |
|
// remembers to add the check, and these services have no CSRF token to fall |
| 31 |
|
// back on — the session cookie is meta.sr.ht's, set on the parent domain, |
| 32 |
|
// with a SameSite no individual service can choose. |
| 33 |
|
// |
| 34 |
|
// Both of chi's routing failures are pointed at this service's error page here, |
| 35 |
|
// so a mistyped URL and a method this router does not serve land on a page with |
| 36 |
|
// a nav rather than on chi's plain-text dead end. The 405 is new: until ecore |
| 37 |
|
// grew chimw.RenderRefusals this service installed the 404 alone and left the |
| 38 |
|
// other refusal to net/http. |
| 39 |
|
// |
| 40 |
|
// Every read route is registered under GET and HEAD both. chi's Get registers |
| 41 |
|
// GET alone, so `curl -I` and every uptime probe used to be answered 405 by |
| 42 |
|
// pages whose whole job is to be cheap to ask about. |
| 43 |
24 |
func (s *Server) Register(r chi.Router) { |
| 44 |
24 |
r.Group(func(r chi.Router) { |
| 45 |
24 |
r.Use(middleware.PrivateCache) |
| 46 |
24 |
r.Use(middleware.RecoverPanics(func(w http.ResponseWriter, r *http.Request, _ any) { |
| 47 |
1 |
s.renderError(w, r, http.StatusInternalServerError, "") |
| 48 |
1 |
})) |
| 49 |
24 |
r.Use(csrf.Require(s.chromeSvc.SelfOrigin(), nil)) |
| 50 |
24 |
|
| 51 |
24 |
chimw.RenderRefusals(r, s.renderError) |
| 52 |
24 |
|
| 53 |
24 |
chimw.GetHead(r, "/", s.handleIndex) |
| 54 |
24 |
chimw.GetHead(r, "/jump", s.handleJump) |
| 55 |
24 |
chimw.GetHead(r, "/healthz", s.handleHealthz) |
| 56 |
24 |
r.Handle(assets.DefaultPrefix+"*", s.static) |
| 57 |
24 |
|
| 58 |
24 |
chimw.GetHead(r, "/~{owner}/{repo}", s.handleRepo) |
| 59 |
24 |
// A single wildcard route serves both the form target (empty wildcard ⇒ |
| 60 |
24 |
// redirect to the canonical URL) and the compare view itself. |
| 61 |
24 |
chimw.GetHead(r, "/~{owner}/{repo}/compare/*", s.handleCompare) |
| 62 |
24 |
chimw.GetHead(r, "/~{owner}/{repo}/commit/{rev}", s.handleCommit) |
| 63 |
|
}) |
| 64 |
|
} |
| 65 |
|
|
| 66 |
|
// handleHealthz is a dependency-free liveness probe. |
| 67 |
2 |
func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) { |
| 68 |
2 |
w.Header().Set("Content-Type", "text/plain; charset=utf-8") |
| 69 |
2 |
_, _ = w.Write([]byte("ok\n")) |
| 70 |
2 |
} |
| 71 |
|
|
| 72 |
|
// handleJump powers the owner/repo jump form: it redirects to the canonical repo |
| 73 |
|
// URL. A leading "~" on the owner is tolerated. |
| 74 |
0 |
func (s *Server) handleJump(w http.ResponseWriter, r *http.Request) { |
| 75 |
0 |
owner := strings.TrimPrefix(strings.TrimSpace(r.URL.Query().Get("owner")), "~") |
| 76 |
0 |
repo := strings.TrimSpace(r.URL.Query().Get("repo")) |
| 77 |
0 |
if owner == "" || repo == "" { |
| 78 |
0 |
s.renderError(w, r, http.StatusBadRequest, "both owner and repository are required") |
| 79 |
0 |
return |
| 80 |
0 |
} |
| 81 |
0 |
http.Redirect(w, r, "/~"+owner+"/"+repo, http.StatusFound) |
| 82 |
|
} |