From 23da433bf4bb99140434706d74201b8875eeff98 Mon Sep 17 00:00:00 2001 From: Dmitry Verkhoturov Date: Sat, 21 Sep 2024 02:49:08 +0100 Subject: [PATCH] Detect and set proper Content-Type for avatars Detect proper avatar type to return instead of returning `image/*`, which is not valid. --- avatar/avatar.go | 18 ++++++++++++++++-- avatar/avatar_test.go | 13 +++++++------ v2/avatar/avatar.go | 18 ++++++++++++++++-- v2/avatar/avatar_test.go | 13 +++++++------ 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/avatar/avatar.go b/avatar/avatar.go index ed691fa..bc8d37b 100644 --- a/avatar/avatar.go +++ b/avatar/avatar.go @@ -23,6 +23,9 @@ import ( "github.com/go-pkgz/auth/token" ) +// http.sniffLen is 512 bytes which is how much we need to read to detect content type +const sniffLen = 512 + // Proxy provides http handler for avatars from avatar.Store // On user login token will call Put and it will retrieve and save picture locally. type Proxy struct { @@ -100,7 +103,6 @@ func (p *Proxy) load(url string, client *http.Client) (rc io.ReadCloser, err err // Handler returns token routes for given provider func (p *Proxy) Handler(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { w.WriteHeader(http.StatusMethodNotAllowed) } @@ -136,9 +138,21 @@ func (p *Proxy) Handler(w http.ResponseWriter, r *http.Request) { } }() - w.Header().Set("Content-Type", "image/*") + buf := make([]byte, sniffLen) + n, err := avReader.Read(buf) + if err != nil && err != io.EOF { + p.Logf("[WARN] can't read from avatar reader for %s, %s", avatarID, err) + rest.SendErrorJSON(w, r, p.L, http.StatusInternalServerError, err, "can't read avatar") + return + } w.Header().Set("Content-Length", strconv.Itoa(size)) + w.Header().Set("Content-Type", http.DetectContentType(buf)) w.WriteHeader(http.StatusOK) + if _, err = w.Write(buf[:n]); err != nil { + p.Logf("[WARN] can't write response to %s, %s", r.RemoteAddr, err) + return + } + // write the rest of response size if it's bigger than 512 bytes, or nothing as EOF would be sent right away then if _, err = io.Copy(w, avReader); err != nil { p.Logf("[WARN] can't send response to %s, %s", r.RemoteAddr, err) } diff --git a/avatar/avatar_test.go b/avatar/avatar_test.go index a5e0bc2..84883b2 100644 --- a/avatar/avatar_test.go +++ b/avatar/avatar_test.go @@ -107,9 +107,10 @@ func TestAvatar_Routes(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/pic.png" { - w.Header().Set("Content-Type", "image/*") + // set wrong content type, in reality would be image/png based on the first bytes + w.Header().Set("Content-Type", "image/jpg") w.Header().Set("Custom-Header", "xyz") - _, err := fmt.Fprint(w, "some picture bin data") + _, err := fmt.Fprint(w, "\x89PNG\x0D\x0A\x1A\x0A some picture bin data") require.NoError(t, err) return } @@ -154,16 +155,16 @@ func TestAvatar_Routes(t *testing.T) { handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, []string{"image/*"}, rr.Header()["Content-Type"]) - assert.Equal(t, []string{"21"}, rr.Header()["Content-Length"]) + assert.Equal(t, []string{"image/png"}, rr.Header()["Content-Type"]) + assert.Equal(t, []string{"30"}, rr.Header()["Content-Length"]) assert.Equal(t, []string(nil), rr.Header()["Custom-Header"], "strip all custom headers") assert.NotNil(t, rr.Header()["Etag"]) bb := bytes.Buffer{} sz, err := io.Copy(&bb, rr.Body) assert.NoError(t, err) - assert.Equal(t, int64(21), sz) - assert.Equal(t, "some picture bin data", bb.String()) + assert.Equal(t, int64(30), sz) + assert.Equal(t, "\x89PNG\x0D\x0A\x1A\x0A some picture bin data", bb.String()) } { diff --git a/v2/avatar/avatar.go b/v2/avatar/avatar.go index 3e9b5c2..f169f52 100644 --- a/v2/avatar/avatar.go +++ b/v2/avatar/avatar.go @@ -23,6 +23,9 @@ import ( "github.com/go-pkgz/auth/v2/token" ) +// http.sniffLen is 512 bytes which is how much we need to read to detect content type +const sniffLen = 512 + // Proxy provides http handler for avatars from avatar.Store // On user login token will call Put and it will retrieve and save picture locally. type Proxy struct { @@ -100,7 +103,6 @@ func (p *Proxy) load(url string, client *http.Client) (rc io.ReadCloser, err err // Handler returns token routes for given provider func (p *Proxy) Handler(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { w.WriteHeader(http.StatusMethodNotAllowed) } @@ -136,9 +138,21 @@ func (p *Proxy) Handler(w http.ResponseWriter, r *http.Request) { } }() - w.Header().Set("Content-Type", "image/*") + buf := make([]byte, sniffLen) + n, err := avReader.Read(buf) + if err != nil && err != io.EOF { + p.Logf("[WARN] can't read from avatar reader for %s, %s", avatarID, err) + rest.SendErrorJSON(w, r, p.L, http.StatusInternalServerError, err, "can't read avatar") + return + } w.Header().Set("Content-Length", strconv.Itoa(size)) + w.Header().Set("Content-Type", http.DetectContentType(buf)) w.WriteHeader(http.StatusOK) + if _, err = w.Write(buf[:n]); err != nil { + p.Logf("[WARN] can't write response to %s, %s", r.RemoteAddr, err) + return + } + // write the rest of response size if it's bigger than 512 bytes, or nothing as EOF would be sent right away then if _, err = io.Copy(w, avReader); err != nil { p.Logf("[WARN] can't send response to %s, %s", r.RemoteAddr, err) } diff --git a/v2/avatar/avatar_test.go b/v2/avatar/avatar_test.go index 937f886..99e4571 100644 --- a/v2/avatar/avatar_test.go +++ b/v2/avatar/avatar_test.go @@ -107,9 +107,10 @@ func TestAvatar_Routes(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/pic.png" { - w.Header().Set("Content-Type", "image/*") + // set wrong content type, in reality would be image/png based on the content of the file + w.Header().Set("Content-Type", "image/jpg") w.Header().Set("Custom-Header", "xyz") - _, err := fmt.Fprint(w, "some picture bin data") + _, err := fmt.Fprint(w, "\x89PNG\x0D\x0A\x1A\x0A some picture bin data") require.NoError(t, err) return } @@ -154,16 +155,16 @@ func TestAvatar_Routes(t *testing.T) { handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, []string{"image/*"}, rr.Header()["Content-Type"]) - assert.Equal(t, []string{"21"}, rr.Header()["Content-Length"]) + assert.Equal(t, []string{"image/png"}, rr.Header()["Content-Type"]) + assert.Equal(t, []string{"30"}, rr.Header()["Content-Length"]) assert.Equal(t, []string(nil), rr.Header()["Custom-Header"], "strip all custom headers") assert.NotNil(t, rr.Header()["Etag"]) bb := bytes.Buffer{} sz, err := io.Copy(&bb, rr.Body) assert.NoError(t, err) - assert.Equal(t, int64(21), sz) - assert.Equal(t, "some picture bin data", bb.String()) + assert.Equal(t, int64(30), sz) + assert.Equal(t, "\x89PNG\x0D\x0A\x1A\x0A some picture bin data", bb.String()) } {