{{.Status}} · {{.Title}}
{{.Message}}
Fehlercode: {{.Code}}
package main import ( "embed" "html/template" "net/http" "strings" ) // Templates werden einkompiliert statt zur Laufzeit gelesen. Zwei Gründe: das // Deployment braucht kein zusätzliches Verzeichnis, und -- wichtiger -- sie // dürfen NICHT unter web/ liegen, weil das komplett per FileServer ausgeliefert // wird (siehe routes()). Eine Vorlage dort wäre über HTTP abrufbar. // //go:embed templates/*.html var tmplFS embed.FS // pages hält je Seite ein eigenes Template-Set aus base.html + der Seitendatei. // Getrennte Sets, weil jede Seite ihren eigenen "content"-Block definiert -- // in einem gemeinsamen Set würde der letzte alle anderen überschreiben. var pages = map[string]*template.Template{} // tmplFuncs.fieldErr vereinheitlicht die beiden Fehlerformen für die Vorlage: // ein Fehler mit einzelnem Field (Login) und einer mit Fields-Map // (Registrierung, alle Felder auf einmal) werden gleich abgefragt. var tmplFuncs = template.FuncMap{ "fieldErr": func(e *Error, field string) string { if e == nil { return "" } if msg, ok := e.Fields[field]; ok { return msg } if e.Field == field { return e.Msg } return "" }, "content": renderContent, "avatar": avatarURL, "profile": profileURL, "time": fmtTime, "timeAttr": fmtTimeAttr, } // pageFiles listet je Seite die zusätzlich benötigten Vorlagen. base.html // kommt überall dazu; Seiten mit Beitragslisten brauchen zusätzlich die // gemeinsamen entry-/pager-Blöcke. var pageFiles = map[string][]string{ "login": nil, "register": nil, "feed": {"templates/entries.html"}, "userfeed": {"templates/entries.html"}, "reported": nil, "thread": {"templates/entries.html"}, "edit": nil, "delete": nil, } func init() { for name, extra := range pageFiles { files := append([]string{"templates/base.html", "templates/" + name + ".html"}, extra...) pages[name] = template.Must( template.New(name).Funcs(tmplFuncs).ParseFS(tmplFS, files...)) } } // pageData ist der Rahmen jeder Seite. Form trägt die zuletzt eingegebenen // Werte, damit ein Fehler den Nutzer nicht sein Formular neu tippen lässt. // ReturnTo ist der Ort, an den Aktions-Formulare zurückführen sollen. type pageData struct { Title string Err *Error Form any ReturnTo string } // render gibt eine Seite mit Status 200 aus. func render(w http.ResponseWriter, r *http.Request, name string, data pageData) { data.ReturnTo = returnTo(r) w.Header().Set("Content-Type", "text/html; charset=utf-8") pages[name].ExecuteTemplate(w, "base", data) } // returnTo bestimmt den Rücksprungort für die Formulare einer Seite. // // Bei GET ist das die Seite selbst -- inklusive Query, damit Filter (Nutzer- // Feed, Suche, Seitenzahl) automatisch mitreisen, ohne dass die Formulare sie // einzeln kennen müssen. Bei POST wird das mitgeschickte return_to // weitergereicht, damit ein neu gerendertes Formular sein Ziel behält. func returnTo(r *http.Request) string { if r.Method == http.MethodGet || r.Method == http.MethodHead { return safeReturn(r.URL.RequestURI(), "/") } return safeReturn(r.FormValue("return_to"), "/") } // safeReturn akzeptiert nur seiteneigene Pfade. Ungeprüft in http.Redirect // gesteckt wäre return_to ein Open-Redirect: ein Link auf die eigene Domain, // der auf einer fremden landet -- die klassische Phishing-Tarnung. // // Abgelehnt wird deshalb alles, was nicht eindeutig ein absoluter Pfad auf // dieser Seite ist: // // - "//host/x" protokoll-relativ, zeigt auf eine fremde Domain // - `/\host/x` manche Browser normalisieren \ zu / -> wie oben // - "https://x" absolute URL // - "u/alice" relativ; würde je nach Basis irgendwo landen // - "\r\n..." Steuerzeichen könnten weitere Header einschleusen func safeReturn(v, fallback string) string { if v == "" || len(v) > 512 { return fallback } if v[0] != '/' || strings.HasPrefix(v, "//") || strings.HasPrefix(v, `/\`) { return fallback } if strings.ContainsAny(v, "\r\n\x00") { return fallback } return v } // redirectBack schickt nach einer erfolgreichen Aktion dorthin zurück, wo sie // ausgelöst wurde -- der noscript-Ersatz für "DOM aktualisieren, Seite stehen // lassen". 303 statt 302, damit der Browser die Zielseite per GET holt und ein // Reload die Aktion nicht wiederholt. func redirectBack(w http.ResponseWriter, r *http.Request, fallback string) { http.Redirect(w, r, safeReturn(r.FormValue("return_to"), fallback), http.StatusSeeOther) } // failForm rendert ein Formular nach einem Fehler neu -- mit Meldung und den // bisherigen Eingaben. Das ist der noscript-Ersatz für die Fehlermeldung, die // das JS-Frontend sonst per fetch nachlädt. // // Serverfehler landen bewusst NICHT im Formular: bei einem 500 ist unklar, ob // die Eingabe überhaupt das Problem war, und ein "bitte nochmal absenden" wäre // die falsche Aufforderung. func failForm(w http.ResponseWriter, r *http.Request, name string, data pageData, err error) { e := asError(err) if e.Kind == KindInternal { renderError(w, e) return } data.Err = e data.ReturnTo = returnTo(r) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(statusOf(e.Kind)) pages[name].ExecuteTemplate(w, "base", data) } // failRequest gibt einen Fehler in dem Format aus, das der Client erwartet. // Für Endpunkte, die beide Seiten bedienen: ein Browser-Formular darf niemals // rohes JSON zu sehen bekommen -- ohne JavaScript gibt es nichts, was es // übersetzen könnte. func failRequest(w http.ResponseWriter, r *http.Request, err error) { if wantsJSON(r) { writeAPIError(w, err) return } renderError(w, err) } // wantsJSON entscheidet, welcher Adapter antwortet. Das JS-Frontend schickt // Accept: application/json (siehe api in feed.js), ein Browser-Formular // schickt text/html -- derselbe Endpunkt bedient damit beide, ohne dass die // Logik doppelt existiert. func wantsJSON(r *http.Request) bool { return strings.Contains(r.Header.Get("Accept"), "application/json") } // HTML-Adapter: das Gegenstück zu writeAPIError. Derselbe Fehler aus derselben // Taxonomie, nur als Seite statt als JSON -- welchen Statuscode er bekommt, // entscheidet weiterhin statusOf, damit die beiden Adapter nicht auseinander // laufen können. // // Die Seite kommt bewusst ohne JavaScript aus: sie ist der Weg, auf dem ein // Nutzer mit abgeschaltetem JS (TOR Browser auf "Safest", I2P) überhaupt // erfährt, was schiefgegangen ist. type errorPageData struct { Status int Title string Message string Code string } var errorTmpl = template.Must(template.New("error").Parse(`
{{.Message}}
Fehlercode: {{.Code}}