From f90af3b07669ec4fdc7f2dd50e23687a32a77092 Mon Sep 17 00:00:00 2001 From: wgroeneveld Date: Thu, 22 Apr 2021 16:02:59 +0200 Subject: [PATCH] built-in genuine image checker based on byte headers --- app/mf/microformats.go | 6 +++ app/pictures/handler.go | 7 +--- app/webmention/recv/receive.go | 36 +++++++++++------ app/webmention/recv/receive_test.go | 16 ++++++-- mocks/picture.bmp | Bin 0 -> 3210 bytes mocks/picture.gif | Bin 0 -> 1751 bytes mocks/picture.png | Bin 0 -> 2845 bytes mocks/picture.tiff | Bin 0 -> 3388 bytes mocks/picture.webp | Bin 0 -> 1002 bytes rest/utils.go | 34 ++++++++++++++++ rest/utils_test.go | 60 ++++++++++++++++++++++++++++ 11 files changed, 138 insertions(+), 21 deletions(-) create mode 100644 mocks/picture.bmp create mode 100644 mocks/picture.gif create mode 100644 mocks/picture.png create mode 100644 mocks/picture.tiff create mode 100644 mocks/picture.webp diff --git a/app/mf/microformats.go b/app/mf/microformats.go index f9edf47..a8de66a 100644 --- a/app/mf/microformats.go +++ b/app/mf/microformats.go @@ -2,6 +2,7 @@ package mf import ( "brainbaking.com/go-jamming/common" + "fmt" "strings" "time" "willnorris.com/go/microformats" @@ -9,6 +10,7 @@ import ( const ( DateFormat = "2006-01-02T15:04:05" + Anonymous = "anonymous" ) type IndiewebAuthor struct { @@ -16,6 +18,10 @@ type IndiewebAuthor struct { Picture string `json:"picture"` } +func (ia IndiewebAuthor) Anonymize() { + ia.Picture = fmt.Sprintf("/pictures/%s", Anonymous) +} + type IndiewebDataResult struct { Status string `json:"status"` Data []*IndiewebData `json:"json"` diff --git a/app/pictures/handler.go b/app/pictures/handler.go index 79be4f1..017de24 100644 --- a/app/pictures/handler.go +++ b/app/pictures/handler.go @@ -1,6 +1,7 @@ package pictures import ( + "brainbaking.com/go-jamming/app/mf" "brainbaking.com/go-jamming/db" _ "embed" "github.com/gorilla/mux" @@ -11,10 +12,6 @@ import ( //go:embed anonymous.jpg var anonymous []byte -const ( - Anonymous = "anonymous" -) - func init() { if anonymous == nil { log.Fatal().Msg("embedded anonymous image missing?") @@ -26,7 +23,7 @@ func init() { func Handle(repo db.MentionRepo) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { picDomain := mux.Vars(r)["picture"] - if picDomain == Anonymous { + if picDomain == mf.Anonymous { servePicture(w, anonymous) return } diff --git a/app/webmention/recv/receive.go b/app/webmention/recv/receive.go index 8329fcc..63b4eae 100644 --- a/app/webmention/recv/receive.go +++ b/app/webmention/recv/receive.go @@ -2,10 +2,10 @@ package recv import ( "brainbaking.com/go-jamming/app/mf" - "brainbaking.com/go-jamming/app/pictures" "brainbaking.com/go-jamming/common" "brainbaking.com/go-jamming/db" "brainbaking.com/go-jamming/rest" + "errors" "fmt" "regexp" "strings" @@ -20,6 +20,12 @@ type Receiver struct { Repo db.MentionRepo } +var ( + errPicUnableToDownload = errors.New("Unable to download author picture") + errPicNoRealImage = errors.New("Downloaded author picture is not a real image") + errPicUnableToSave = errors.New("Unable to save downloaded author picture") +) + func (recv *Receiver) Receive(wm mf.Mention) { log.Info().Stringer("wm", wm).Msg("OK: looks valid") _, body, geterr := recv.RestClient.GetBody(wm.Source) @@ -42,12 +48,16 @@ func (recv *Receiver) processSourceBody(body string, wm mf.Mention) { data := microformats.Parse(strings.NewReader(body), wm.SourceUrl()) indieweb := recv.convertBodyToIndiewebData(body, wm, mf.HEntry(data)) if indieweb.Author.Picture != "" { - recv.saveAuthorPictureLocally(indieweb) + err := recv.saveAuthorPictureLocally(indieweb) + if err != nil { + log.Error().Err(err).Str("url", indieweb.Author.Picture).Msg("Failed to save picture. Reverting to anonymous") + indieweb.Author.Anonymize() + } } key, err := recv.Repo.Save(wm, indieweb) if err != nil { - log.Error().Err(err).Stringer("wm", wm).Msg("processSourceBody: failed to save json to db") + log.Error().Err(err).Stringer("wm", wm).Msg("Failed to save new mention to db") } log.Info().Str("key", key).Msg("OK: Webmention processed.") } @@ -96,26 +106,26 @@ func (recv *Receiver) parseBodyAsNonIndiewebSite(body string, wm mf.Mention) *mf } } -// saveAuthorPictureLocally tries to download the author picture. +// saveAuthorPictureLocally tries to download the author picture and checks if it's valid based on img header. // If it succeeds, it alters the picture path to a local /pictures/x one. -// If it fails, it falls back to a local default image. -// This *should* also validate image byte headers, like https://stackoverflow.com/questions/670546/determine-if-file-is-an-image -func (recv *Receiver) saveAuthorPictureLocally(indieweb *mf.IndiewebData) { +// If it fails, it returns an error. +func (recv *Receiver) saveAuthorPictureLocally(indieweb *mf.IndiewebData) error { _, picData, err := recv.RestClient.GetBody(indieweb.Author.Picture) if err != nil { - log.Warn().Err(err).Str("url", indieweb.Author.Picture).Msg("Unable to download author picture. Reverting to anonymous.") - indieweb.Author.Picture = fmt.Sprintf("/pictures/%s", pictures.Anonymous) - return + return errPicUnableToDownload } + if len(picData) < 8 || !rest.IsRealImage([]byte(picData[0:8])) { + return errPicNoRealImage + } + srcDomain := rest.Domain(indieweb.Source) _, dberr := recv.Repo.SavePicture(picData, srcDomain) if dberr != nil { - log.Warn().Err(err).Str("url", indieweb.Author.Picture).Msg("Unable to save downloaded author picture. Reverting to anonymous.") - indieweb.Author.Picture = fmt.Sprintf("/pictures/%s", pictures.Anonymous) - return + return errPicUnableToSave } indieweb.Author.Picture = fmt.Sprintf("/pictures/%s", srcDomain) + return nil } func nonIndiewebTitle(body string, wm mf.Mention) string { diff --git a/app/webmention/recv/receive_test.go b/app/webmention/recv/receive_test.go index 3da7d11..0f4cbc5 100644 --- a/app/webmention/recv/receive_test.go +++ b/app/webmention/recv/receive_test.go @@ -27,16 +27,25 @@ func TestSaveAuthorPictureLocally(t *testing.T) { label string pictureUrl string expectedPictureUrl string + expectedError error }{ { "Absolute URL gets 'downloaded' and replaced by relative", "https://brainbaking.com/picture.jpg", "/pictures/brainbaking.com", + nil, }, { - "Absolute URL gets replaced by anonymous if download fails", + "Absolute URL does not get replaced but error if no valid image", + "https://brainbaking.com/index.xml", + "https://brainbaking.com/index.xml", + errPicNoRealImage, + }, + { + "Absolute URL does not get replaced but error if download fails", "https://brainbaking.com/thedogatemypic-nowitsmissing-shiii.png", - "/pictures/anonymous", + "https://brainbaking.com/thedogatemypic-nowitsmissing-shiii.png", + errPicUnableToDownload, }, } @@ -57,9 +66,10 @@ func TestSaveAuthorPictureLocally(t *testing.T) { Picture: tc.pictureUrl, }, } - recv.saveAuthorPictureLocally(indieweb) + err := recv.saveAuthorPictureLocally(indieweb) assert.Equal(t, tc.expectedPictureUrl, indieweb.Author.Picture) + assert.Equal(t, tc.expectedError, err) }) } } diff --git a/mocks/picture.bmp b/mocks/picture.bmp new file mode 100644 index 0000000000000000000000000000000000000000..c4bd6c8f934a103bc8024ceff1537ff065834a19 GIT binary patch literal 3210 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 Z2t(TFLV&Ow`gb-ot-)G-Cnyv+`9C<%L+k(m literal 0 HcmV?d00001 diff --git a/mocks/picture.png b/mocks/picture.png new file mode 100644 index 0000000000000000000000000000000000000000..aa4df1b966f8cf995c5a8c1d20fb33f399c7c5fa GIT binary patch literal 2845 zcmV+&3*z*NP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x010qNS#tmY3labT3lag+-G2N4016*TL_t(Y$1Rn6aFz9) zhd;l|dpqakBqvt_1V{o51TX@jAVpn8%2wJEuT`8?9J=G~cIzLzx^``6UAMD4?X)^| zXYH1mZLRIN?by|=P{9f)N)QzRisvP4WW zFhqnhLtx+m5U?N)f;JEifTD#0JYgoas4XD~Fz?VTVh>oPUBnDPQ2?5Ff_cmuXaaMY zB?3So4g+wbED@6~BZ@$ranFJnjJ2`g38TR!KwwIb^qd;Mc8U2_n|9n^4x=EjuwkU6 z=VeG~CJ10)Qd$imHIXsoae@$>qRH7CdLN%nqG+ z|GySAX08q`UH_#As0=3IhyW%6H()F&0?^#l)ajw&Q};Z*|JFT=5KEG{I;OeMyLa8) z-#1iOpLt~Oqh58r0vK5Z#D;|C9=GT_|7J9OY9>2n#hO3A=V@p_kcmd8vO^=6FJuzj z=tzb0+3Zz65ViHKO)cA<^acor7{I_x46a}m94|y*0Y-`d11Tba0okGVuAVvQ`-z)( zJpo%gdF}Jw?!yzapSN#V&5dfPNce8#5HZ&+M0&bp=W}v+FkfH2X7la$!A}vv7$x0E zTWwJxAifa500mf2QqPA6{;Kod(_{T`oI`K?DwUYJWqqwv>#I{{WqW zkO08I!OQRzpa?`@UZ!%(wl!xi9=PkPEsYHpx%m0LKYsf0|NiK}71ya2P7EW!s3Z+C z!I|S{+R`1($%>!;v&Hm(RWVA5TGP7=XdDJO?*3zEhbi zp45e**-yK|zuWgO=ekB`CvspBsH8|+;Q(G`vgWOWC$CHuZ|!^-F!MbpFuX@LL^sEG*|Hg4DwK&*uN;!51u?C`oUgco|NgjvN{I z5Pp<1US-Qi2ah|BvuVeMvL1CHr-hzySvKwtzx5SBjgKHafp z9TkfbDax73n_9m9$nv^Y_0s;Q5Kw+DzB+ybNWS-n<6gSvx4(X2>qDzI-CM8df@`v( zs5rSOkKVOw$-tF^u8d(Jr33&Fn4v=Ma^UCSSLs>fI8}g8(-RLpyZ0d>Kpu*d38!-C z;>F>C*{MSG{?S8~8IJr|u`uhpzTiF_2i!tsnW(6#puzt0OImg^O$L?=0(g12|4%GB z-_%G@5p)vpNNDDUaa4lkuJl2mIW}oTLtC3wc}@}L3KVlCoG>oK#6HuE@=k5V{H)tM zwrJzF$0T8%vkU-OGuU^wwW$E*K@%u8P|WiSnlb4oxP^}4I;=X=AS-&#U+L``bd_jo zXbzPrQ0^A2FNW&2qb*zOi;BcSC z)05*pL%r9dV#&Y#wg>BOYOAbiacUajF(3r zWCF5w;qnq)FH2#C7BCLkq$)Ab4=1A2z2Wdo(=B&?f9KW(a5Q8DxD;_n0j5-dLA;t)HICcMo`#^&1}8 z_r&6s`U@xDKlAZ1Lezh~FOu<*3>wHnc=!&5>7L`dStCWU#}TIh z4XNQ9K>j~v7X~VZ#mmorFI83b*wfEg?vWc1ft@}3smSNrH*VFA)E<0Hf2p>vo}s1z zYd76=W74Mm#jDyjH#9F_+ul}F6GXzgekJ2c8-tNCx*SIN58gZc$%jWSobDQ%C_eLV z&)xIwXA}jH;DEe&!}0^K{;s`ao8-`dhI#n0JCpgz{@&q}?_S_BJbkJA-6Nrr6%}=o zNhIdD2|OgErrg|AXd>+(M4Yb0=B;gaePegQtAgOhKoSHXE#g1haofSyf3tMmElb*0 zR5+q-{o0*(e;J{j>KVyR&J6cm8tA!Xj6Xdd&W;yDXHMEYGd!Ji)~?8SzQ1P6md2G$ zGx5aDyYJ8ieX2VLV=l-@6Z-RYcXYJg z(|QN(M81qXLnyPQLFuPJJe~58@*om$($oFD6=esh^5si|>BI^cE*ZcqnBp!^n#cus zv5-wU1fZdMF_S*h>2mBTA^vKNk*vbV>`@Xh| zrbp`&k~BGqs$8i!lbx)ss_-2*)WYG!b?gvha73bFsyyXZYsJd_1GMMyZ$JKnmyi%K zQ;dW|Tml7#bZi|*I5iDTH4O`3ROh#LZ&>7vcb}OaEly4P##R{{lAUIh%gQQYtZTJL z(i+DIuP)iVbVa(Qxgl^Hd15uVFccXUyDSNV{{s+IQweGuxL*JO03~!qSaf7zbY(hY za%Ew3WdJfTGBGVMG%YbXR53F;Ff}?cI4dwPIxsMisKQ79001R)MObuXVRU6WZEs|0 vW_bWIFfuVMFf=VOIaDz+Ix{yqFgPnPFgh?WY~B^q00000NkvXXu0mjfzm{B@ literal 0 HcmV?d00001 diff --git a/mocks/picture.tiff b/mocks/picture.tiff new file mode 100644 index 0000000000000000000000000000000000000000..19614840bd0018110ddb338152bbf99675577485 GIT binary patch literal 3388 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