Fix delete color and contrast

This commit is contained in:
2026-09-13 10:08:04 +02:00
parent 9c5aae5386
commit 600e66f2af
9 changed files with 42 additions and 36 deletions
+6 -6
View File
@@ -61,12 +61,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.handler.S
func (s *Server) routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", s.handleIndex)
mux.HandleFunc("POST /api/upload", s.handleUpload)
mux.HandleFunc("POST /upload", s.handleUpload)
mux.HandleFunc("GET /api/limits", s.handleLimits)
mux.HandleFunc("GET /admin", s.handleAdmin)
mux.HandleFunc("GET /d/{id}", s.handleDownload)
mux.HandleFunc("GET /i/{id}", s.handleInfo)
mux.HandleFunc("POST /api/d/{id}/delete", s.handleDelete)
mux.HandleFunc("POST /d/{id}/delete", s.handleDelete)
mux.HandleFunc("GET /login", s.handleLoginPage)
mux.HandleFunc("POST /login", s.handleLogin)
mux.HandleFunc("POST /logout", s.handleLogout)
@@ -104,10 +104,10 @@ func (s *Server) staticHandler() http.Handler {
// appCSP locks the application pages down to their own origin. The frontend has
// no inline script and no third-party anything, so this can be strict.
//
// connect-src is not optional here: the upload page talks to /api/upload and
// /api/limits over XMLHttpRequest, and every fetch-directive left unlisted
// falls back to default-src, so omitting it makes the browser block every
// upload before it reaches the network. See TestAppCSPAllowsWhatThePageDoes.
// connect-src is not optional here: the upload page posts to /upload over
// XMLHttpRequest to draw a progress bar, and every fetch-directive left
// unlisted falls back to default-src, so omitting it makes the browser block
// every upload before it reaches the network. See TestAppCSPAllowsWhatThePageDoes.
const appCSP = "default-src 'none'; script-src 'self'; style-src 'self'; " +
"img-src 'self' data:; connect-src 'self'; form-action 'self'; " +
"base-uri 'none'; frame-ancestors 'none'"
+21 -21
View File
@@ -110,7 +110,7 @@ func (h *harness) upload(t *testing.T, body []byte, headers map[string]string) *
func (h *harness) uploadReader(t *testing.T, body io.Reader, headers map[string]string) *http.Response {
t.Helper()
req, err := http.NewRequest("POST", h.ts.URL+"/api/upload", body)
req, err := http.NewRequest("POST", h.ts.URL+"/upload", body)
if err != nil {
t.Fatal(err)
}
@@ -416,7 +416,7 @@ func TestPathTraversalIsRejected(t *testing.T) {
func TestReservedNamesAreRejected(t *testing.T) {
h := newHarness(t, nil)
for _, name := range []string{"api", "static", "d", "i", "robots.txt"} {
for _, name := range []string{"api", "static", "d", "i", "upload", "robots.txt"} {
resp := h.upload(t, []byte("x"), map[string]string{
"Vanity": name,
"Authorization": "Bearer " + h.token,
@@ -434,7 +434,7 @@ func TestDeleteRequiresTheRightToken(t *testing.T) {
del := func(token string) int {
form := strings.NewReader("token=" + token)
req, _ := http.NewRequest("POST", h.ts.URL+"/api/d/"+res.ID+"/delete", form)
req, _ := http.NewRequest("POST", h.ts.URL+"/d/"+res.ID+"/delete", form)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
resp, err := h.ts.Client().Do(req)
@@ -465,7 +465,7 @@ func TestAdminMayDeleteAnything(t *testing.T) {
h := newHarness(t, nil)
res := decode[uploadResult](t, h.upload(t, []byte("someone else's"), nil))
req, _ := http.NewRequest("POST", h.ts.URL+"/api/d/"+res.ID+"/delete", nil)
req, _ := http.NewRequest("POST", h.ts.URL+"/d/"+res.ID+"/delete", nil)
req.Header.Set("Authorization", "Bearer "+h.admin)
req.Header.Set("Accept", "application/json")
resp, err := h.ts.Client().Do(req)
@@ -494,7 +494,7 @@ func TestMultipartUpload(t *testing.T) {
fw.Write([]byte("hello from a browser"))
mw.Close()
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", &body)
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", &body)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Accept", "application/json")
resp, err := h.ts.Client().Do(req)
@@ -523,7 +523,7 @@ func TestFormPostRendersHTML(t *testing.T) {
fw.Write([]byte("data"))
mw.Close()
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", &body)
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", &body)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Accept", "text/html,application/xhtml+xml")
resp, err := h.ts.Client().Do(req)
@@ -693,7 +693,7 @@ func TestLoginStoresTheToken(t *testing.T) {
}
// The session alone is now enough to claim a custom name.
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", strings.NewReader("two"))
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", strings.NewReader("two"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Vanity", "session-upload")
req.AddCookie(cookie)
@@ -885,7 +885,7 @@ func TestCookieDoesNotShadowTheDeleteToken(t *testing.T) {
res := decode[uploadResult](t, h.upload(t, []byte("x"), nil))
form := strings.NewReader("token=" + res.DeleteToken)
req, _ := http.NewRequest("POST", h.ts.URL+"/api/d/"+res.ID+"/delete", form)
req, _ := http.NewRequest("POST", h.ts.URL+"/d/"+res.ID+"/delete", form)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
req.AddCookie(&http.Cookie{Name: tokenCookie, Value: h.token})
@@ -929,7 +929,7 @@ func (h *harness) formUploadWith(t *testing.T, cookie *http.Cookie, fields map[s
fw.Write([]byte(content))
mw.Close()
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", &body)
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", &body)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Accept", "application/json")
if cookie != nil {
@@ -1050,7 +1050,7 @@ func TestResultPageOffersBothLinksAndAWayBack(t *testing.T) {
fw.Write([]byte("data"))
mw.Close()
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", &body)
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", &body)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Accept", "text/html")
resp, err := h.ts.Client().Do(req)
@@ -1223,7 +1223,7 @@ func TestAdminDeleteReturnsToTheTable(t *testing.T) {
client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
form := strings.NewReader("from=admin")
req, _ := http.NewRequest("POST", h.ts.URL+"/api/d/"+res.ID+"/delete", form)
req, _ := http.NewRequest("POST", h.ts.URL+"/d/"+res.ID+"/delete", form)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "text/html")
req.AddCookie(&http.Cookie{Name: tokenCookie, Value: h.admin})
@@ -1250,7 +1250,7 @@ func TestAdminDeleteStillRequiresAdmin(t *testing.T) {
res := decode[uploadResult](t, h.upload(t, []byte("not yours"), nil))
form := strings.NewReader("from=admin")
req, _ := http.NewRequest("POST", h.ts.URL+"/api/d/"+res.ID+"/delete", form)
req, _ := http.NewRequest("POST", h.ts.URL+"/d/"+res.ID+"/delete", form)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
req.AddCookie(&http.Cookie{Name: tokenCookie, Value: h.token})
@@ -1305,7 +1305,7 @@ func TestMultipartHonoursTheHeaderForm(t *testing.T) {
fw.Write([]byte("payload"))
mw.Close()
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", &body)
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", &body)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+h.token)
@@ -1339,7 +1339,7 @@ func TestFormFieldsOverrideTheHeaders(t *testing.T) {
fw.Write([]byte("payload"))
mw.Close()
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", &body)
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", &body)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+h.token)
@@ -1399,7 +1399,7 @@ func TestInfoPageAcceptsTheDeleteToken(t *testing.T) {
t.Fatal("the info page leaks the delete token to anyone holding the link")
}
resp := h.postForm(t, "/api/d/"+res.ID+"/delete",
resp := h.postForm(t, "/d/"+res.ID+"/delete",
url.Values{"from": {"info"}, "token": {res.DeleteToken}}, nil)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
@@ -1417,7 +1417,7 @@ func TestWrongDeleteTokenReturnsToTheInfoPage(t *testing.T) {
res := decode[uploadResult](t, h.upload(t, []byte("x"), map[string]string{
"Content-Disposition": `attachment; filename="keepme.bin"`}))
resp := h.postForm(t, "/api/d/"+res.ID+"/delete",
resp := h.postForm(t, "/d/"+res.ID+"/delete",
url.Values{"from": {"info"}, "token": {"wrong"}}, nil)
raw, _ := io.ReadAll(resp.Body)
resp.Body.Close()
@@ -1473,7 +1473,7 @@ func TestInfoPageOffersADirectButtonToAnOwner(t *testing.T) {
}
// And that button actually works with no token field at all.
resp := h.postForm(t, "/api/d/"+res.ID+"/delete",
resp := h.postForm(t, "/d/"+res.ID+"/delete",
url.Values{"from": {"info"}}, &http.Cookie{Name: tokenCookie, Value: h.token})
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
@@ -1494,7 +1494,7 @@ func TestFailedDeletesAreThrottledAndSuccessIsNot(t *testing.T) {
if last != nil {
last.Body.Close()
}
last = h.postForm(t, "/api/d/"+res.ID+"/delete",
last = h.postForm(t, "/d/"+res.ID+"/delete",
url.Values{"from": {"info"}, "token": {"guess"}}, nil)
}
if last.StatusCode != http.StatusTooManyRequests {
@@ -1503,7 +1503,7 @@ func TestFailedDeletesAreThrottledAndSuccessIsNot(t *testing.T) {
last.Body.Close()
// The real token still works, having consumed nothing from the bucket.
resp := h.postForm(t, "/api/d/"+res.ID+"/delete",
resp := h.postForm(t, "/d/"+res.ID+"/delete",
url.Values{"from": {"info"}, "token": {res.DeleteToken}}, nil)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
@@ -1520,7 +1520,7 @@ func TestFailedDeletesAreThrottledAndSuccessIsNot(t *testing.T) {
func TestCrossOriginPostsAreRejected(t *testing.T) {
h := newHarness(t, nil)
paths := []string{"/login", "/logout", "/api/upload", "/api/d/anything/delete"}
paths := []string{"/login", "/logout", "/upload", "/d/anything/delete"}
hostile := []map[string]string{
{"Origin": "https://evil.example.com"},
{"Sec-Fetch-Site": "cross-site"},
@@ -1746,7 +1746,7 @@ func TestChosenPassphraseWorksEndToEnd(t *testing.T) {
t.Fatal("no session was started")
}
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", strings.NewReader("x"))
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", strings.NewReader("x"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Vanity", "chosen-upload")
req.AddCookie(cookie)
+1 -1
View File
@@ -405,7 +405,7 @@ func (s *Server) respondUploaded(w http.ResponseWriter, r *http.Request, m *stor
URL: url,
InfoURL: s.absBase(r) + "i/" + m.ID,
DeleteToken: secret,
DeleteURL: s.absBase(r) + "api/d/" + m.ID + "/delete",
DeleteURL: s.absBase(r) + "d/" + m.ID + "/delete",
})
return
}
+1 -1
View File
@@ -17,7 +17,7 @@ var vanityRe = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{1,63}$`)
// allowed into the object namespace.
var reserved = map[string]bool{
"d": true, "i": true, "api": true, "static": true, "admin": true,
"login": true, "logout": true,
"login": true, "logout": true, "upload": true,
"favicon.ico": true, "robots.txt": true, "index.html": true,
"sitemap.xml": true, "tokens.json": true, "objects": true,
}