Files
2026-09-18 20:56:41 +02:00

159 lines
4.5 KiB
Go

package server
import (
"encoding/json"
"fmt"
"net/http"
"time"
"uncensored-send/internal/auth"
"uncensored-send/internal/config"
"uncensored-send/internal/store"
)
func writeJSON(w http.ResponseWriter, status int, v any) {
b, err := json.Marshal(v)
if err != nil {
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
w.Write(append(b, '\n'))
}
func (s *Server) absBase(r *http.Request) string {
if s.cfg.PublicURL != "" {
return s.cfg.PublicURL + s.cfg.BasePath
}
scheme := "http"
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
scheme = "https"
}
return scheme + "://" + r.Host + s.cfg.BasePath
}
func (s *Server) objectURL(r *http.Request, id string) string {
return s.absBase(r) + "d/" + id
}
func (s *Server) infoURL(r *http.Request, id string) string {
return s.absBase(r) + "i/" + id
}
type indexPage struct {
page
MaxSize string
MaxExpiry string
DefaultExpiry string
// ExpiryHint is the same lifetime as DefaultExpiry, worded for the form's
// "Expires in" field rather than for a row in the limits list: what the
// field wants is "never", what the list wants is "unlimited".
ExpiryHint string
AbsBase string
Source string // the repository this build came from, "" to say nothing
AllowVanity bool
MaxSizeBytes int64 // 0 when unlimited; the script checks against it
Stale bool // a login whose token no longer exists
}
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
lim, err := s.limitsFor(r, s.cookieCredential(r))
stale := false
switch {
case err != nil:
// The token was revoked or the file was edited; end the session rather
// than leave the caller wondering why uploads fail.
s.forget(w, r)
lim, stale = auth.Anonymous(s.cfg), true
case s.staleSession(r):
// A cookie this server cannot open. Same treatment: it is not a
// session, and it should stop being sent.
s.forget(w, r)
stale = true
}
s.render(w, http.StatusOK, "index.html", indexPage{
page: s.page(r, "Upload", true),
MaxSize: config.FormatSize(lim.MaxSize),
MaxExpiry: config.FormatLifetime(lim.MaxExpiry),
DefaultExpiry: config.FormatLifetime(lim.DefaultExpiry),
ExpiryHint: config.FormatDuration(lim.DefaultExpiry),
AbsBase: s.absBase(r),
Source: s.cfg.SourceURL,
AllowVanity: lim.AllowVanity,
MaxSizeBytes: lim.MaxSize,
Stale: stale,
})
}
type objectPage struct {
page
Meta *store.Meta
Size string
Expires string
URL string
InfoURL string
DeleteToken string
// CanDelete is set when the viewer's own token already authorises removing
// this file, so they are offered a button instead of a token field.
CanDelete bool
Error string
}
func (s *Server) handleInfo(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
}
s.renderInfo(w, r, m, http.StatusOK, "")
}
// renderInfo draws the file's page, optionally with an error from a failed
// delete attempt, so a mistyped token lands back on the form rather than on a
// dead end.
func (s *Server) renderInfo(w http.ResponseWriter, r *http.Request, m *store.Meta, status int, errMsg string) {
infoURL := s.infoURL(r, m.ID)
head := s.page(r, m.Filename, true)
// This is the page people paste at each other, so it is the one that has
// to unfurl into something readable.
head.Preview = objectPreview(m, infoURL)
s.render(w, status, "info.html", objectPage{
page: head,
Meta: m,
Size: config.FormatSize(m.Size),
Expires: describeExpiry(m.Expires, s.now()),
URL: s.objectURL(r, m.ID),
InfoURL: infoURL,
CanDelete: s.mayDelete(r, m, s.credential(r)),
Error: errMsg,
})
}
// describeExpiry renders a deadline as an absolute time plus how far off it is.
func describeExpiry(t *time.Time, now time.Time) string {
if t == nil {
return "never"
}
at := t.UTC().Format("2006-01-02 15:04 MST")
d := t.Sub(now).Round(time.Minute)
switch {
case d < 0:
return "expired"
case d == 0:
// Rounded away to nothing, and a zero duration is how this program
// spells "unlimited": saying "in never" of a file about to go would
// be exactly backwards.
return at + " (in under a minute)"
}
return fmt.Sprintf("%s (in %s)", at, config.FormatDuration(d))
}