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')) } // absBase is the absolute URL of this service, for links and examples. // // It prefers --public-url. Falling back to the request's Host is display-only: // the header is caller-controlled, so it is never used for anything a third // party would see. 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 } // objectURL builds the download link handed back to an uploader. func (s *Server) objectURL(r *http.Request, id string) string { return s.absBase(r) + "d/" + 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 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) { // A remembered token is resolved server-side, so the page can show the real // limits without the cookie ever being readable by a script. lim, err := s.limitsFor(r, cookieCredential(r)) stale := false if 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 } 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), 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) { s.render(w, status, "info.html", objectPage{ page: s.page(r, m.Filename, true), Meta: m, Size: config.FormatSize(m.Size), Expires: describeExpiry(m.Expires, s.now()), URL: s.objectURL(r, m.ID), CanDelete: s.mayDelete(r, m, 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)) }