135 lines
4.3 KiB
Go
135 lines
4.3 KiB
Go
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(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)
|
|
}
|