Initial commit
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
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
|
||||
}
|
||||
|
||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
// The page always renders the anonymous tier; the script refreshes it from
|
||||
// /api/limits once a token is entered.
|
||||
lim := auth.Anonymous(s.cfg)
|
||||
s.render(w, http.StatusOK, "index.html", indexPage{
|
||||
page: s.page("Upload", true),
|
||||
MaxSize: config.FormatSize(lim.MaxSize),
|
||||
MaxExpiry: config.FormatDuration(lim.MaxExpiry),
|
||||
DefaultExpiry: config.FormatDuration(lim.DefaultExpiry),
|
||||
AbsBase: s.absBase(r),
|
||||
})
|
||||
}
|
||||
|
||||
type limitsJSON struct {
|
||||
Name string `json:"name"`
|
||||
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(bearer(r))
|
||||
if err != nil {
|
||||
s.fail(w, r, http.StatusUnauthorized, "Unrecognised token.")
|
||||
return
|
||||
}
|
||||
out := limitsJSON{
|
||||
Name: lim.Name,
|
||||
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
|
||||
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(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))
|
||||
}
|
||||
Reference in New Issue
Block a user