Compare commits
2
Commits
d4da322031
...
c190dd9ea6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c190dd9ea6 | ||
|
|
8901a3c82c |
@@ -91,17 +91,6 @@ func (l *limitReader) Read(p []byte) (int, error) {
|
|||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// isImageMime reports whether the sniffed mime is a raster image the viewer
|
|
||||||
// can render inline (#221). SVG is excluded: it is forced to text/plain on
|
|
||||||
// serving by the active-content rule and must never render as an image.
|
|
||||||
func isImageMime(mime string) bool {
|
|
||||||
switch mime {
|
|
||||||
case "image/png", "image/jpeg", "image/gif", "image/webp":
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleCreatePasteMultipart implements POST /api/pastes with
|
// handleCreatePasteMultipart implements POST /api/pastes with
|
||||||
// multipart/form-data (#38). Fields mirror the JSON create path; a 'file'
|
// multipart/form-data (#38). Fields mirror the JSON create path; a 'file'
|
||||||
// part makes the paste a file paste (1 file = 1 paste: if text content is
|
// part makes the paste a file paste (1 file = 1 paste: if text content is
|
||||||
|
|||||||
@@ -306,3 +306,64 @@ func TestMultipartPasswordFieldAccepted(t *testing.T) {
|
|||||||
t.Fatalf("paste should require password, got %d", rec2.Code)
|
t.Fatalf("paste should require password, got %d", rec2.Code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #281: /raw/{id} must stream the attachment blob for ALL attachment mimes,
|
||||||
|
// not just raster images (the old isImageMime gate left non-image
|
||||||
|
// attachments serving an empty body from row.Content).
|
||||||
|
func TestRawStreamsNonImageAttachment(t *testing.T) {
|
||||||
|
s := testServer(t)
|
||||||
|
h := s.routes()
|
||||||
|
|
||||||
|
body := []byte("hello, this is a plain text attachment body")
|
||||||
|
rec, resp := multipartCreate(t, h, "notes.txt", body, nil)
|
||||||
|
if rec.Code != 201 {
|
||||||
|
t.Fatalf("create: %d %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
if resp["attachment"] == nil {
|
||||||
|
t.Fatalf("no attachment in response: %v", resp)
|
||||||
|
}
|
||||||
|
id, _ := resp["id"].(string)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/raw/"+id, nil)
|
||||||
|
rec2 := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec2, req)
|
||||||
|
if rec2.Code != 200 {
|
||||||
|
t.Fatalf("raw: %d %s", rec2.Code, rec2.Body.String())
|
||||||
|
}
|
||||||
|
if got := rec2.Header().Get("Content-Type"); got != "text/plain; charset=utf-8" {
|
||||||
|
t.Fatalf("Content-Type = %q", got)
|
||||||
|
}
|
||||||
|
if got := rec2.Header().Get("X-Content-Type-Options"); got != "nosniff" {
|
||||||
|
t.Fatalf("nosniff = %q", got)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(rec2.Body.Bytes(), body) {
|
||||||
|
t.Fatalf("raw bytes differ: got %d bytes want %d", rec2.Body.Len(), len(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #281: active-content attachment types still get forced to text/plain on
|
||||||
|
// /raw, same rule as the /f/ serving path (#34).
|
||||||
|
func TestRawHtmlAttachmentServesAsPlainText(t *testing.T) {
|
||||||
|
s := testServer(t)
|
||||||
|
h := s.routes()
|
||||||
|
|
||||||
|
html := []byte("<html><body><script>alert(1)</script></body></html>")
|
||||||
|
rec, resp := multipartCreate(t, h, "page.html", html, nil)
|
||||||
|
if rec.Code != 201 {
|
||||||
|
t.Fatalf("create: %d %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
id, _ := resp["id"].(string)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/raw/"+id, nil)
|
||||||
|
rec2 := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec2, req)
|
||||||
|
if rec2.Code != 200 {
|
||||||
|
t.Fatalf("raw: %d %s", rec2.Code, rec2.Body.String())
|
||||||
|
}
|
||||||
|
if got := rec2.Header().Get("Content-Type"); got != "text/plain; charset=utf-8" {
|
||||||
|
t.Fatalf("Content-Type = %q", got)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(rec2.Body.Bytes(), html) {
|
||||||
|
t.Fatal("raw bytes differ from upload")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -501,15 +501,18 @@ func (a *apiServer) handleRaw(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "not found", 404)
|
http.Error(w, "not found", 404)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// #221: raw view of an image paste serves the image bytes themselves as
|
// #221: raw view of a paste backed by an attachment serves the stored
|
||||||
// an image, not the (empty) text content.
|
// blob bytes with the sniffed mime, not the (empty) text content — for
|
||||||
if att, err := a.store.GetAttachmentForPaste(row.ID); err == nil && att != nil && isImageMime(att.Mime) {
|
// ALL attachment mimes (#281); /raw/{id} is the raw fetch for the file
|
||||||
|
// too. serveContentType still forces active-content types (html, svg,
|
||||||
|
// xml) to text/plain per the #34 rule below.
|
||||||
|
if att, err := a.store.GetAttachmentForPaste(row.ID); err == nil && att != nil {
|
||||||
blobs := a.store.Blobs()
|
blobs := a.store.Blobs()
|
||||||
if blobs != nil {
|
if blobs != nil {
|
||||||
if blob, err := blobs.Get(row.ID + "/" + att.SHA256); err == nil {
|
if blob, err := blobs.Get(row.ID + "/" + att.SHA256); err == nil {
|
||||||
defer blob.Close()
|
defer blob.Close()
|
||||||
a.store.IncrementViews(row.ID, "", 0) // raw views always count (#49/#95)
|
a.store.IncrementViews(row.ID, "", 0) // raw views always count (#49/#95)
|
||||||
w.Header().Set("Content-Type", att.Mime)
|
w.Header().Set("Content-Type", serveContentType(att.Mime))
|
||||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", att.Size))
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", att.Size))
|
||||||
http.ServeContent(w, r, "", time.Unix(att.CreatedAt, 0), blob)
|
http.ServeContent(w, r, "", time.Unix(att.CreatedAt, 0), blob)
|
||||||
|
|||||||
Reference in New Issue
Block a user