package server import ( "encoding/json" "fmt" "net/http" "time" "send/internal/auth" "send/internal/config" "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 AbsBase string TokenName string // the remembered token's name, if there is one AllowVanity bool Stale bool // a remembered token that 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. remembered := cookieCredential(r) lim, err := s.limitsFor(remembered) stale := false if err != nil { // The token was revoked or the file was edited; drop the cookie 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.FormatDuration(lim.MaxExpiry), DefaultExpiry: config.FormatDuration(lim.DefaultExpiry), AbsBase: s.absBase(r), TokenName: lim.Name, AllowVanity: lim.AllowVanity, Stale: stale, }) } type limitsJSON struct { Name string `json:"name"` Remembered bool `json:"remembered"` MaxSize *int64 `json:"max_size"` // null means unlimited MaxExpiry string `json:"max_expiry"` DefaultExpiry string `json:"default_expiry"` AllowVanity bool `json:"allow_vanity"` } // handleLimits reports the permissions belonging to the presented credential, // so the page can show what the caller may actually do. func (s *Server) handleLimits(w http.ResponseWriter, r *http.Request) { // Rate-limited like an upload: this is the one endpoint that reports // whether a token is valid, and it should not be a free oracle. if !s.limiter.allow(clientIP(r, s.cfg), s.now()) { s.fail(w, r, http.StatusTooManyRequests, "Too many requests; try again shortly.") return } lim, err := s.limitsFor(credential(r)) if err != nil { s.fail(w, r, http.StatusUnauthorized, "Unrecognised token.") return } out := limitsJSON{ Name: lim.Name, Remembered: lim.Name != "" && bearer(r) == "", MaxExpiry: config.FormatDuration(lim.MaxExpiry), DefaultExpiry: config.FormatDuration(lim.DefaultExpiry), AllowVanity: lim.AllowVanity, } if lim.MaxSize != config.Unlimited { n := lim.MaxSize out.MaxSize = &n } writeJSON(w, http.StatusOK, out) } type objectPage struct { page Meta *store.Meta Size string Expires string URL string InfoURL string DeleteToken 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.render(w, http.StatusOK, "info.html", objectPage{ page: s.page(r, m.Filename, false), Meta: m, Size: config.FormatSize(m.Size), Expires: describeExpiry(m.Expires, s.now()), }) } // 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" } d := t.Sub(now).Round(time.Minute) if d < 0 { return "expired" } return fmt.Sprintf("%s (in %s)", t.UTC().Format("2006-01-02 15:04 MST"), config.FormatDuration(d)) }