Migration
This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs.
Contact me
internal/core/route.go
3a50036d96eb14462e96598fd169d1c79264ac4b
· 4.5 KB · 157 lines
raw
| 1 | package core |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "html/template" |
| 8 | "net/http" |
| 9 | ) |
| 10 | |
| 11 | // TemplateSet holds one independently-parsed template tree per view. |
| 12 | // Views are parsed separately because html/template has a single namespace |
| 13 | // for defined blocks -- two views both defining "main" would collide. |
| 14 | type TemplateSet map[string]*template.Template |
| 15 | |
| 16 | // Renderer turns a Ctx into bytes plus a content type. HTML pages and JSON |
| 17 | // endpoints are the same shape of thing; this is where they unify. |
| 18 | type Renderer interface { |
| 19 | Render(c *Ctx, ts TemplateSet) ([]byte, error) |
| 20 | ContentType() string |
| 21 | } |
| 22 | |
| 23 | // View renders an HTML template. The template calls modules via `mod`. |
| 24 | type View struct{ Template string } |
| 25 | |
| 26 | func (v View) ContentType() string { return "text/html; charset=utf-8" } |
| 27 | |
| 28 | func (v View) Render(c *Ctx, ts TemplateSet) ([]byte, error) { |
| 29 | t, ok := ts[v.Template] |
| 30 | if !ok { |
| 31 | return nil, fmt.Errorf("no such view %q", v.Template) |
| 32 | } |
| 33 | // Clone per render so the funcmap can close over this Ctx. |
| 34 | tt, err := t.Clone() |
| 35 | if err != nil { |
| 36 | return nil, err |
| 37 | } |
| 38 | tt = tt.Funcs(FuncMap(c)) |
| 39 | var buf bytes.Buffer |
| 40 | if err := tt.ExecuteTemplate(&buf, v.Template, c); err != nil { |
| 41 | return nil, fmt.Errorf("view %s: %w", v.Template, err) |
| 42 | } |
| 43 | return buf.Bytes(), nil |
| 44 | } |
| 45 | |
| 46 | // Endpoint builds the value that will be serialized. It must return a |
| 47 | // purpose-built DTO, never a raw module value -- see internal/api. |
| 48 | type Endpoint func(c *Ctx) (any, error) |
| 49 | |
| 50 | // JSON renders an API endpoint. |
| 51 | type JSON struct{ Build Endpoint } |
| 52 | |
| 53 | func (j JSON) ContentType() string { return "application/json; charset=utf-8" } |
| 54 | |
| 55 | func (j JSON) Render(c *Ctx, _ TemplateSet) ([]byte, error) { |
| 56 | v, err := j.Build(c) |
| 57 | if err != nil { |
| 58 | return nil, err |
| 59 | } |
| 60 | b, err := json.MarshalIndent(v, "", " ") |
| 61 | if err != nil { |
| 62 | return nil, err |
| 63 | } |
| 64 | return append(b, '\n'), nil |
| 65 | } |
| 66 | |
| 67 | // ------------------------------------------------------------------ routes |
| 68 | |
| 69 | // Route binds a renderer to a net/http pattern (Go 1.22 wildcards). |
| 70 | // |
| 71 | // /{repo}/tree/{rest...} -> r.PathValue("repo"), r.PathValue("rest") |
| 72 | // |
| 73 | // Scope decides how the Ctx is built before Render runs. |
| 74 | type Route struct { |
| 75 | Name string |
| 76 | Pattern string // e.g. "GET /{repo}/commit/{rev}" |
| 77 | Render Renderer |
| 78 | Scope Scope |
| 79 | // Immutable marks responses whose content cannot change for a given |
| 80 | // URL -- anything addressed by a commit SHA. Those get a year-long |
| 81 | // Cache-Control; ref-addressed URLs get revalidation instead. |
| 82 | Immutable bool |
| 83 | } |
| 84 | |
| 85 | type Scope int |
| 86 | |
| 87 | const ( |
| 88 | ScopeRepo Scope = iota // resolves {repo}, and {rest...}/{rev} if present |
| 89 | ScopeSite // no repository in the URL |
| 90 | ) |
| 91 | |
| 92 | // httpError lets a module or endpoint choose the status code. |
| 93 | type httpError struct { |
| 94 | Code int |
| 95 | Err error |
| 96 | } |
| 97 | |
| 98 | func (e httpError) Error() string { return e.Err.Error() } |
| 99 | func (e httpError) Unwrap() error { return e.Err } |
| 100 | |
| 101 | func Errorf(code int, format string, a ...any) error { |
| 102 | return httpError{Code: code, Err: fmt.Errorf(format, a...)} |
| 103 | } |
| 104 | |
| 105 | func statusOf(err error) int { |
| 106 | var he httpError |
| 107 | if ok := asHTTPError(err, &he); ok { |
| 108 | return he.Code |
| 109 | } |
| 110 | return http.StatusInternalServerError |
| 111 | } |
| 112 | |
| 113 | // HeaderSetter lets a renderer add response headers. Optional: the server |
| 114 | // type-asserts for it after Render succeeds. |
| 115 | type HeaderSetter interface { |
| 116 | Headers(c *Ctx, h http.Header) |
| 117 | } |
| 118 | |
| 119 | // Raw serves file bytes straight out of the object database. |
| 120 | // |
| 121 | // SECURITY: a repository can contain an .html file with a <script> tag. If |
| 122 | // this served it as text/html on your own origin, any push would be stored |
| 123 | // XSS against the whole site -- which is exactly why GitHub puts raw content |
| 124 | // on raw.githubusercontent.com, a separate origin. Serving it from the same |
| 125 | // host is only safe if the response cannot be interpreted as a document: |
| 126 | // text/plain or octet-stream, nosniff so the browser does not second-guess |
| 127 | // that, and a sandbox CSP as a backstop. |
| 128 | type Raw struct{ Module string } |
| 129 | |
| 130 | func (Raw) ContentType() string { return "text/plain; charset=utf-8" } |
| 131 | |
| 132 | func (r Raw) Render(c *Ctx, _ TemplateSet) ([]byte, error) { |
| 133 | v, err := c.Mod(r.Module) |
| 134 | if err != nil { |
| 135 | return nil, err |
| 136 | } |
| 137 | b, ok := v.(interface{ Bytes() []byte }) |
| 138 | if !ok { |
| 139 | return nil, fmt.Errorf("raw: module %q is not raw-serving", r.Module) |
| 140 | } |
| 141 | return b.Bytes(), nil |
| 142 | } |
| 143 | |
| 144 | func (r Raw) Headers(c *Ctx, h http.Header) { |
| 145 | h.Set("X-Content-Type-Options", "nosniff") |
| 146 | h.Set("Content-Security-Policy", "default-src 'none'; sandbox") |
| 147 | h.Set("Content-Disposition", "inline") |
| 148 | |
| 149 | v, err := c.Mod(r.Module) |
| 150 | if err != nil { |
| 151 | return |
| 152 | } |
| 153 | if bin, ok := v.(interface{ IsBinary() bool }); ok && bin.IsBinary() { |
| 154 | h.Set("Content-Type", "application/octet-stream") |
| 155 | h.Set("Content-Disposition", "attachment") |
| 156 | } |
| 157 | } |