From f7ded3d432d6d54873eb19e75f1d21ea3b613251 Mon Sep 17 00:00:00 2001 From: Travis Ralston <travpc@gmail.com> Date: Mon, 2 Sep 2019 21:16:30 -0600 Subject: [PATCH] Add option to serve quarantine thumbnail for media downloads Fixes https://github.com/turt2live/matrix-media-repo/issues/182 --- common/config/config.go | 3 + config.sample.yaml | 6 ++ .../download_controller.go | 27 ++++++ .../quarantine_controller.go | 89 +++++++++++++++++++ .../thumbnail_controller.go | 79 +--------------- 5 files changed, 127 insertions(+), 77 deletions(-) create mode 100644 controllers/quarantine_controller/quarantine_controller.go diff --git a/common/config/config.go b/common/config/config.go index a1a8fc99..b56ad892 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -113,6 +113,7 @@ type CacheConfig struct { type QuarantineConfig struct { ReplaceThumbnails bool `yaml:"replaceThumbnails"` + ReplaceDownloads bool `yaml:"replaceDownloads"` ThumbnailPath string `yaml:"thumbnailPath"` AllowLocalAdmins bool `yaml:"allowLocalAdmins"` } @@ -308,7 +309,9 @@ func NewDefaultConfig() *MediaRepoConfig { }, Quarantine: &QuarantineConfig{ ReplaceThumbnails: true, + ReplaceDownloads: false, ThumbnailPath: "", + AllowLocalAdmins: true, }, TimeoutSeconds: &TimeoutsConfig{ UrlPreviews: 10, diff --git a/config.sample.yaml b/config.sample.yaml index bbb83aa3..b1a21f7d 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -288,6 +288,12 @@ quarantine: # not affect regular downloads of files. replaceThumbnails: true + # If true, when media which has been quarantined is requested an image will be returned. If + # no image is given in the thumbnailPath below then a generated image will be provided. This + # will replace media which is not an image (ie: quarantining a PDF will replace the PDF with + # an image). + replaceDownloads: false + # If provided, the given image will be returned as a thumbnail for media that is quarantined. #thumbnailPath: "/path/to/thumbnail.png" diff --git a/controllers/download_controller/download_controller.go b/controllers/download_controller/download_controller.go index 4cf85eec..7ea4acb3 100644 --- a/controllers/download_controller/download_controller.go +++ b/controllers/download_controller/download_controller.go @@ -1,16 +1,20 @@ package download_controller import ( + "bytes" "context" "database/sql" "errors" "fmt" "time" + "github.com/disintegration/imaging" "github.com/patrickmn/go-cache" "github.com/sirupsen/logrus" "github.com/turt2live/matrix-media-repo/common" + "github.com/turt2live/matrix-media-repo/common/config" "github.com/turt2live/matrix-media-repo/common/globals" + "github.com/turt2live/matrix-media-repo/controllers/quarantine_controller" "github.com/turt2live/matrix-media-repo/internal_cache" "github.com/turt2live/matrix-media-repo/storage" "github.com/turt2live/matrix-media-repo/storage/datastore" @@ -64,6 +68,29 @@ func GetMedia(origin string, mediaId string, downloadRemote bool, blockForMedia if media != nil { if media.Quarantined { log.Warn("Quarantined media accessed") + + if config.Get().Quarantine.ReplaceDownloads { + log.Info("Replacing thumbnail with a quarantined one") + + img, err := quarantine_controller.GenerateQuarantineThumbnail(512, 512) + if err != nil { + return nil, err + } + + data := &bytes.Buffer{} + imaging.Encode(data, img, imaging.PNG) + return &types.MinimalMedia{ + // Lie about all the details + Stream: util.BufferToStream(data), + ContentType: "image/png", + UploadName: "quarantine.png", + SizeBytes: int64(data.Len()), + MediaId: mediaId, + Origin: origin, + KnownMedia: media, + }, nil + } + return nil, common.ErrMediaQuarantined } diff --git a/controllers/quarantine_controller/quarantine_controller.go b/controllers/quarantine_controller/quarantine_controller.go new file mode 100644 index 00000000..d757428e --- /dev/null +++ b/controllers/quarantine_controller/quarantine_controller.go @@ -0,0 +1,89 @@ +package quarantine_controller + +import ( + "bytes" + "image" + "image/color" + "math" + + "github.com/disintegration/imaging" + "github.com/fogleman/gg" + "github.com/golang/freetype/truetype" + "github.com/turt2live/matrix-media-repo/common/config" + "golang.org/x/image/font/gofont/gosmallcaps" +) + +func GenerateQuarantineThumbnail(width int, height int) (image.Image, error) { + var centerImage image.Image + var err error + if config.Get().Quarantine.ThumbnailPath != "" { + centerImage, err = imaging.Open(config.Get().Quarantine.ThumbnailPath) + } else { + centerImage, err = generateDefaultQuarantineThumbnail() + } + if err != nil { + return nil, err + } + + c := gg.NewContext(width, height) + + centerImage = imaging.Fit(centerImage, width, height, imaging.Lanczos) + + c.DrawImageAnchored(centerImage, width/2, height/2, 0.5, 0.5) + + buf := &bytes.Buffer{} + c.EncodePNG(buf) + + return imaging.Decode(buf) +} + +func generateDefaultQuarantineThumbnail() (image.Image, error) { + c := gg.NewContext(700, 700) + c.Clear() + + red := color.RGBA{R: 190, G: 26, B: 25, A: 255} + orange := color.RGBA{R: 255, G: 186, B: 73, A: 255} + x := 350.0 + y := 300.0 + r := 256.0 + w := 55.0 + p := 64.0 + m := "media not allowed" + + c.SetColor(orange) + c.DrawRectangle(0, 0, 700, 700) + c.Fill() + + c.SetColor(red) + c.DrawCircle(x, y, r) + c.Fill() + + c.SetColor(color.White) + c.DrawCircle(x, y, r-w) + c.Fill() + + lr := r - (w / 2) + sx := x + (lr * math.Cos(gg.Radians(225.0))) + sy := y + (lr * math.Sin(gg.Radians(225.0))) + ex := x + (lr * math.Cos(gg.Radians(45.0))) + ey := y + (lr * math.Sin(gg.Radians(45.0))) + c.SetLineCap(gg.LineCapButt) + c.SetLineWidth(w) + c.SetColor(red) + c.DrawLine(sx, sy, ex, ey) + c.Stroke() + + f, err := truetype.Parse(gosmallcaps.TTF) + if err != nil { + panic(err) + } + + c.SetColor(color.Black) + c.SetFontFace(truetype.NewFace(f, &truetype.Options{Size: 64})) + c.DrawStringAnchored(m, x, y+r+p, 0.5, 0.5) + + buf := &bytes.Buffer{} + c.EncodePNG(buf) + + return imaging.Decode(buf) +} diff --git a/controllers/thumbnail_controller/thumbnail_controller.go b/controllers/thumbnail_controller/thumbnail_controller.go index e91f954a..72c65b3d 100644 --- a/controllers/thumbnail_controller/thumbnail_controller.go +++ b/controllers/thumbnail_controller/thumbnail_controller.go @@ -5,14 +5,9 @@ import ( "context" "database/sql" "fmt" - "image" - "image/color" - "math" "time" "github.com/disintegration/imaging" - "github.com/fogleman/gg" - "github.com/golang/freetype/truetype" "github.com/patrickmn/go-cache" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -20,12 +15,12 @@ import ( "github.com/turt2live/matrix-media-repo/common/config" "github.com/turt2live/matrix-media-repo/common/globals" "github.com/turt2live/matrix-media-repo/controllers/download_controller" + "github.com/turt2live/matrix-media-repo/controllers/quarantine_controller" "github.com/turt2live/matrix-media-repo/internal_cache" "github.com/turt2live/matrix-media-repo/storage" "github.com/turt2live/matrix-media-repo/storage/datastore" "github.com/turt2live/matrix-media-repo/types" "github.com/turt2live/matrix-media-repo/util" - "golang.org/x/image/font/gofont/gosmallcaps" ) // These are the content types that we can actually thumbnail @@ -66,7 +61,7 @@ func GetThumbnail(origin string, mediaId string, desiredWidth int, desiredHeight if config.Get().Quarantine.ReplaceThumbnails { log.Info("Replacing thumbnail with a quarantined one") - img, err := GenerateQuarantineThumbnail(desiredWidth, desiredHeight) + img, err := quarantine_controller.GenerateQuarantineThumbnail(desiredWidth, desiredHeight) if err != nil { return nil, err } @@ -221,76 +216,6 @@ func GetOrGenerateThumbnail(media *types.Media, width int, height int, animated return result.thumbnail, result.err } -func GenerateQuarantineThumbnail(width int, height int) (image.Image, error) { - var centerImage image.Image - var err error - if config.Get().Quarantine.ThumbnailPath != "" { - centerImage, err = imaging.Open(config.Get().Quarantine.ThumbnailPath) - } else { - centerImage, err = generateDefaultQuarantineThumbnail() - } - if err != nil { - return nil, err - } - - c := gg.NewContext(width, height) - - centerImage = imaging.Fit(centerImage, width, height, imaging.Lanczos) - - c.DrawImageAnchored(centerImage, width/2, height/2, 0.5, 0.5) - - buf := &bytes.Buffer{} - c.EncodePNG(buf) - - return imaging.Decode(buf) -} - -func generateDefaultQuarantineThumbnail() (image.Image, error) { - c := gg.NewContext(700, 700) - c.Clear() - - red := color.RGBA{R: 190, G: 26, B: 25, A: 255} - x := 350.0 - y := 300.0 - r := 256.0 - w := 55.0 - p := 64.0 - m := "media not allowed" - - c.SetColor(red) - c.DrawCircle(x, y, r) - c.Fill() - - c.SetColor(color.White) - c.DrawCircle(x, y, r-w) - c.Fill() - - lr := r - (w / 2) - sx := x + (lr * math.Cos(gg.Radians(225.0))) - sy := y + (lr * math.Sin(gg.Radians(225.0))) - ex := x + (lr * math.Cos(gg.Radians(45.0))) - ey := y + (lr * math.Sin(gg.Radians(45.0))) - c.SetLineCap(gg.LineCapButt) - c.SetLineWidth(w) - c.SetColor(red) - c.DrawLine(sx, sy, ex, ey) - c.Stroke() - - f, err := truetype.Parse(gosmallcaps.TTF) - if err != nil { - panic(err) - } - - c.SetColor(color.Black) - c.SetFontFace(truetype.NewFace(f, &truetype.Options{Size: 64})) - c.DrawStringAnchored(m, x, y+r+p, 0.5, 0.5) - - buf := &bytes.Buffer{} - c.EncodePNG(buf) - - return imaging.Decode(buf) -} - func pickThumbnailDimensions(desiredWidth int, desiredHeight int, desiredMethod string) (int, int, string, error) { if desiredWidth <= 0 { return 0, 0, "", errors.New("width must be positive") -- GitLab