package server import ( "net/http" "strings" "uncensored-send/internal/auth" "uncensored-send/internal/store" ) // handleDelete removes an object early. Three credentials are accepted: the // delete token handed to the uploader, the token that owns the object, and any // admin token. // // There is only one delete route, and it is a POST, so the success page's plain // form works with scripting disabled and no second code path is needed. func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request) { id, err := store.CleanID(r.PathValue("id")) if err != nil { s.fail(w, r, http.StatusNotFound, "No such file.") return } m, err := s.store.Get(id, s.now()) if err != nil { s.fail(w, r, http.StatusNotFound, "No such file.") return } presented := s.deleteCredentials(w, r) from := r.PostFormValue("from") if len(presented) == 0 { s.refuse(w, r, m, from, http.StatusUnauthorized, "A delete token or an owning token is required.") return } if !s.authorised(r, m, presented) { // Only failures are throttled, so a correct token is never delayed. // The info page is publicly shareable and now carries a credential // field, which is reason enough not to let it be hammered freely. if !s.authLimiter.allow(clientIP(r, s.cfg), s.now()) { s.refuse(w, r, m, from, http.StatusTooManyRequests, "Too many failed attempts; try again shortly.") return } s.refuse(w, r, m, from, http.StatusForbidden, "That delete token is not correct.") return } if err := s.store.Delete(id); err != nil { s.log.Error("deleting object", "id", id, "err", err) s.fail(w, r, http.StatusInternalServerError, "Could not delete the file.") return } s.log.Info("deleted", "id", id, "ip", clientIP(r, s.cfg)) if wantsJSON(r) { writeJSON(w, http.StatusOK, map[string]string{"status": "deleted", "id": id}) return } // Deleting from the listing goes back to it. The destination is built from // configuration, never from the request, so this cannot be turned into an // open redirect. if from == "files" { http.Redirect(w, r, s.cfg.BasePath+"files", http.StatusSeeOther) return } s.render(w, http.StatusOK, "error.html", errorPage{ page: s.page(r, "Deleted", false), Status: "Deleted", Message: "The file is gone.", }) } // refuse reports a rejected deletion. A failed attempt from the file's own page // lands back on that page with the reason, rather than on a generic error page // that has thrown away what the reader typed. func (s *Server) refuse(w http.ResponseWriter, r *http.Request, m *store.Meta, from string, status int, msg string) { if from == "info" && !wantsJSON(r) { s.renderInfo(w, r, m, status, msg) return } s.fail(w, r, status, msg) } // deleteCredentials collects every secret the request carries. // // Three can legitimately arrive at once, the object's delete token in the // form, a token in the header, and a remembered token in the cookie, and any // one of them may be the sufficient one. They are all collected so that the // first one present cannot shadow the others. func (s *Server) deleteCredentials(w http.ResponseWriter, r *http.Request) []string { var out []string add := func(secret string) { if secret = strings.TrimSpace(secret); secret != "" { out = append(out, secret) } } add(bearer(r)) add(s.cookieCredential(r)) // A small form post; the cap keeps this from being a way to stream a body // into memory. A non-form body simply fails to parse and is ignored. r.Body = http.MaxBytesReader(w, r.Body, maxFieldBytes) if err := r.ParseForm(); err == nil { add(r.PostFormValue("token")) } return out } // authorised reports whether any of the presented secrets may delete m. func (s *Server) authorised(r *http.Request, m *store.Meta, presented []string) bool { for _, secret := range presented { if s.mayDelete(r, m, secret) { return true } } return false } // mayDelete checks one secret against the object's delete token first - which // is always a generated value, so that comparison is cheap - and only then // against the token file, which may cost a derivation. func (s *Server) mayDelete(r *http.Request, m *store.Meta, secret string) bool { if auth.EqualHash(m.DeleteHash, auth.HashSecret(secret)) { return true } lim, err := s.limitsFor(r, secret) if err != nil { return false } return lim.Admin || (m.Owner != "" && lim.Name == m.Owner) }