diff options
Diffstat (limited to 'internal/web/server.go')
| -rw-r--r-- | internal/web/server.go | 93 |
1 files changed, 69 insertions, 24 deletions
diff --git a/internal/web/server.go b/internal/web/server.go index 4bb408e..f005b76 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -187,36 +187,57 @@ func (s *Server) session(next http.Handler) http.Handler { }) } -// requireAuth redirects anonymous users to the login page. -func (s *Server) requireAuth(next http.HandlerFunc) http.HandlerFunc { +// requireLogin sends anonymous users to the login page and nothing else; the +// body cap and the CSRF check come in guardPOST, once the upload limit is known. +func (s *Server) requireLogin(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - u := currentUser(r) - if u == nil { + if currentUser(r) == nil { http.Redirect(w, r, "/webadmin?next="+r.URL.Path, http.StatusSeeOther) return } - if r.Method == http.MethodPost { - // Cap the request body before any form parsing (uploads included). - r.Body = http.MaxBytesReader(w, r.Body, s.cfg.MaxUploadBytes+1<<20) - if err := parseForm(r); err != nil { - var tooBig *http.MaxBytesError - if errors.As(err, &tooBig) { - s.plainError(w, http.StatusRequestEntityTooLarge, s.trf(r, "Upload too large: the limit is %d MB.", s.cfg.MaxUploadBytes>>20)) - return - } - s.plainError(w, http.StatusBadRequest, s.tr(r, "Could not read the form.")) - return - } - if !auth.CheckCSRF(s.cfg.JWTSecret, u.ID, u.TokenVersion, r.FormValue("_csrf")) { - s.plainError(w, http.StatusForbidden, s.tr(r, "Form expired or invalid. Go back, reload the page and try again.")) - return - } - } next(w, r) } } +// guardPOST caps a POST body at limit plus 1 MB of form overhead, parses it and +// checks the CSRF token; false means an error response was written. Other +// methods pass straight through. +func (s *Server) guardPOST(w http.ResponseWriter, r *http.Request, limit int64) bool { + if r.Method != http.MethodPost { + return true + } + u := currentUser(r) + // Cap the request body before any form parsing (uploads included). + r.Body = http.MaxBytesReader(w, r.Body, limit+1<<20) + if err := parseForm(r); err != nil { + var tooBig *http.MaxBytesError + if errors.As(err, &tooBig) { + s.fail(w, r, http.StatusRequestEntityTooLarge, s.trf(r, "Upload too large: the limit is %d MB.", limit>>20)) + return false + } + s.fail(w, r, http.StatusBadRequest, s.tr(r, "Could not read the form.")) + return false + } + if !auth.CheckCSRF(s.cfg.JWTSecret, u.ID, u.TokenVersion, r.FormValue("_csrf")) { + s.fail(w, r, http.StatusForbidden, s.tr(r, "Form expired or invalid. Go back, reload the page and try again.")) + return false + } + return true +} + +// requireAuth is for management pages outside a blog (dashboard, password, +// admin): logged in, small forms only. +func (s *Server) requireAuth(next http.HandlerFunc) http.HandlerFunc { + return s.requireLogin(func(w http.ResponseWriter, r *http.Request) { + if s.guardPOST(w, r, 0) { + next(w, r) + } + }) +} + // parseForm parses urlencoded or multipart bodies, surfacing size errors. +// Multipart parts beyond 1 MB in total spill to temp files, which net/http +// removes once the handler returns. func parseForm(r *http.Request) error { ct := r.Header.Get("Content-Type") if strings.HasPrefix(ct, "multipart/form-data") { @@ -225,6 +246,20 @@ func parseForm(r *http.Request) error { return r.ParseForm() } +// wantsJSON is how the upload scripts ask for answers they can parse. +func wantsJSON(r *http.Request) bool { + return strings.Contains(r.Header.Get("Accept"), "application/json") +} + +// fail answers an error as JSON when the client asked for it, else as the plain page. +func (s *Server) fail(w http.ResponseWriter, r *http.Request, status int, msg string) { + if wantsJSON(r) { + writeJSON(w, status, map[string]string{"error": msg}) + return + } + s.plainError(w, status, msg) +} + func (s *Server) requireAdmin(next http.HandlerFunc) http.HandlerFunc { return s.requireAuth(func(w http.ResponseWriter, r *http.Request) { if !currentUser(r).IsSuperadmin() { @@ -235,9 +270,16 @@ func (s *Server) requireAdmin(next http.HandlerFunc) http.HandlerFunc { }) } -// withBlog resolves /b/{sub}/... and enforces owner-or-superadmin. +// withBlog resolves /b/{sub}/..., enforces owner-or-superadmin and caps a POST +// at the blog's own upload limit. func (s *Server) withBlog(next http.HandlerFunc) http.HandlerFunc { - return s.requireAuth(func(w http.ResponseWriter, r *http.Request) { + return s.withBlogFiles(1, next) +} + +// withBlogFiles is withBlog for a form that may carry up to n files at once +// (the Files page's multi-upload): the body cap is n limits. +func (s *Server) withBlogFiles(n int, next http.HandlerFunc) http.HandlerFunc { + return s.requireLogin(func(w http.ResponseWriter, r *http.Request) { u := currentUser(r) r, err := s.resolveBlog(r, r.PathValue("sub")) if err != nil { @@ -257,7 +299,10 @@ func (s *Server) withBlog(next http.HandlerFunc) http.HandlerFunc { if blog.OwnerID != u.ID { lang = s.userLang(r, u) } - next(w, withLang(r, lang)) + r = withLang(r, lang) + if s.guardPOST(w, r, int64(n)*blog.UploadLimit(s.cfg)) { + next(w, r) + } }) } |
