All checks were successful
check / check (push) Successful in 3m6s
chi runs Use middleware in registration order, and every form route
group registered CSRF() before MaxBodySize(). gorilla/csrf calls
r.PostFormValue, so the form was parsed under net/http's 10 MB default
and the intended 1 MB cap never applied to form fields. The
/user/{username} group, which carries POST /password, had no
MaxBodySize registration at all.
- Register MaxBodySize ahead of CSRF in /pages, /sources, and
/source/{sourceID}, and add it to /user/{username}.
- Reject a declared-oversize body up front with 413. Reordering alone
cannot produce one: http.MaxBytesReader surfaces its error on Read,
so the form parse fails and gorilla/csrf answers 403 "no token" for
what is really an oversized body. MaxBytesReader is still installed
afterwards so chunked or length-lying clients stay hard-capped.
- Drop the handler-local MaxBytesReader calls in auth.go, profile.go,
and source_management.go now that the middleware is the single
enforcement point. maxBodyShift stays; webhook.go still uses it.
The /webhook/{uuid} receiver is untouched: it bounds itself with
io.LimitReader in readWebhookBody and is neither CSRF-protected nor
form-parsed.
Tests cover the middleware in isolation (declared oversize is rejected
without reaching a sentinel handler; at-limit and under-limit bodies
pass through intact; GET is unaffected; an undeclared oversize body is
truncated at the cap) and the real router built by SetupRoutes, so the
registration order itself is guarded: an oversized POST to
/pages/login returns 413 with no gorilla/csrf cookie issued, an
oversized POST /password with a valid session and CSRF token returns
413 and leaves the stored hash unchanged, and under-limit requests
still complete through the normal CSRF path.
219 lines
4.5 KiB
Go
219 lines
4.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// HandleLoginPage returns a handler for the login page (GET)
|
|
func (h *Handlers) HandleLoginPage() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// Check if already logged in
|
|
sess, err := h.session.Get(r)
|
|
if err == nil && h.session.IsAuthenticated(sess) {
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
|
|
return
|
|
}
|
|
|
|
// Render login page
|
|
data := map[string]any{
|
|
tmplKeyError: "",
|
|
}
|
|
|
|
h.renderTemplate(w, r, "login.html", data)
|
|
}
|
|
}
|
|
|
|
// HandleLoginSubmit handles the login form submission (POST)
|
|
func (h *Handlers) HandleLoginSubmit() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// The body size cap is enforced by the MaxBodySize
|
|
// middleware, which runs before CSRF parses the form.
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
h.log.Error("failed to parse form", "error", err)
|
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
|
|
|
return
|
|
}
|
|
|
|
username := r.FormValue("username")
|
|
password := r.FormValue("password")
|
|
|
|
// Validate input
|
|
if username == "" || password == "" {
|
|
h.renderLoginError(
|
|
w, r,
|
|
"Username and password are required",
|
|
http.StatusBadRequest,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
user, err := h.authenticateUser(
|
|
w, r, username, password,
|
|
)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = h.createAuthenticatedSession(w, r, user)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
h.log.Info(
|
|
"user logged in",
|
|
"username", username,
|
|
"user_id", user.ID,
|
|
)
|
|
|
|
// Redirect to home page
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
// renderLoginError renders the login page with an error message.
|
|
func (h *Handlers) renderLoginError(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
msg string,
|
|
status int,
|
|
) {
|
|
data := map[string]any{
|
|
tmplKeyError: msg,
|
|
}
|
|
|
|
w.WriteHeader(status)
|
|
h.renderTemplate(w, r, "login.html", data)
|
|
}
|
|
|
|
// authenticateUser looks up and verifies a user's credentials.
|
|
// On failure it writes an HTTP response and returns an error.
|
|
func (h *Handlers) authenticateUser(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
username, password string,
|
|
) (database.User, error) {
|
|
var user database.User
|
|
|
|
err := h.db.DB().Where(
|
|
"username = ?", username,
|
|
).First(&user).Error
|
|
if err != nil {
|
|
h.log.Debug("user not found", "username", username)
|
|
h.renderLoginError(
|
|
w, r,
|
|
"Invalid username or password",
|
|
http.StatusUnauthorized,
|
|
)
|
|
|
|
return user, err
|
|
}
|
|
|
|
valid, err := database.VerifyPassword(password, user.Password)
|
|
if err != nil {
|
|
h.log.Error("failed to verify password", "error", err)
|
|
http.Error(
|
|
w, "Internal server error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return user, err
|
|
}
|
|
|
|
if !valid {
|
|
h.log.Debug("invalid password", "username", username)
|
|
h.renderLoginError(
|
|
w, r,
|
|
"Invalid username or password",
|
|
http.StatusUnauthorized,
|
|
)
|
|
|
|
return user, errInvalidPassword
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// createAuthenticatedSession regenerates the session and stores
|
|
// user info. On failure it writes an HTTP response and returns
|
|
// an error.
|
|
func (h *Handlers) createAuthenticatedSession(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
user database.User,
|
|
) error {
|
|
oldSess, err := h.session.Get(r)
|
|
if err != nil {
|
|
h.log.Error("failed to get session", "error", err)
|
|
http.Error(
|
|
w, "Internal server error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
sess, err := h.session.Regenerate(r, w, oldSess)
|
|
if err != nil {
|
|
h.log.Error(
|
|
"failed to regenerate session", "error", err,
|
|
)
|
|
http.Error(
|
|
w, "Internal server error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
h.session.SetUser(sess, user.ID, user.Username)
|
|
|
|
err = h.session.Save(r, w, sess)
|
|
if err != nil {
|
|
h.log.Error("failed to save session", "error", err)
|
|
http.Error(
|
|
w, "Internal server error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// HandleLogout handles user logout
|
|
func (h *Handlers) HandleLogout() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
sess, err := h.session.Get(r)
|
|
if err != nil {
|
|
h.log.Error("failed to get session", "error", err)
|
|
http.Redirect(
|
|
w, r, "/pages/login", http.StatusSeeOther,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
// Destroy session
|
|
h.session.Destroy(sess)
|
|
|
|
// Save the destroyed session
|
|
err = h.session.Save(r, w, sess)
|
|
if err != nil {
|
|
h.log.Error(
|
|
"failed to save destroyed session",
|
|
"error", err,
|
|
)
|
|
}
|
|
|
|
// Redirect to login page
|
|
http.Redirect(w, r, "/pages/login", http.StatusSeeOther)
|
|
}
|
|
}
|