Skip to content

Commit

Permalink
Add a basic way to quarantine media
Browse files Browse the repository at this point in the history
Part of #45 - the only thing missing is a way to quarantine all media in a room
  • Loading branch information
turt2live committed Jan 21, 2018
1 parent 775f242 commit 4141301
Show file tree
Hide file tree
Showing 15 changed files with 286 additions and 16 deletions.
10 changes: 10 additions & 0 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,13 @@ rateLimit:
# this feature is completely optional.
identicons:
enabled: true

# The quarantine media settings.
quarantine:
# If true, when a thumbnail of quarantined media is requested an image will be returned. If no
# image is given in the thumbnailPath below then a generated image will be provided. This does
# not affect regular downloads of files.
replaceThumbnails: true

# If provided, the given image will be returned as a thumbnail for media that is quarantined.
#thumbnailPath: "/path/to/thumbnail.png"
1 change: 1 addition & 0 deletions migrations/3_add_quarantine_flag_down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE media DROP COLUMN quarantined;
1 change: 1 addition & 0 deletions migrations/3_add_quarantine_flag_up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE media ADD COLUMN quarantined BOOL NOT NULL DEFAULT FALSE;
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ func DownloadMedia(w http.ResponseWriter, r *http.Request, log *logrus.Entry) in
return client.NotFoundError()
} else if err == errs.ErrMediaTooLarge {
return client.RequestTooLarge()
} else if err == errs.ErrMediaQuarantined {
return client.NotFoundError() // We lie for security
}
log.Error("Unexpected error locating media: " + err.Error())
return client.InternalServerError("Unexpected Error")
Expand Down
61 changes: 61 additions & 0 deletions src/github.com/turt2live/matrix-media-repo/client/r0/quarantine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package r0

import (
"net/http"

"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"github.com/turt2live/matrix-media-repo/client"
"github.com/turt2live/matrix-media-repo/matrix"
"github.com/turt2live/matrix-media-repo/services/media_service"
"github.com/turt2live/matrix-media-repo/util"
)

type MediaQuarantinedResponse struct {
IsQuarantined bool `json:"is_quarantined"`
}

func QuarantineMedia(w http.ResponseWriter, r *http.Request, log *logrus.Entry) interface{} {
accessToken := util.GetAccessTokenFromRequest(r)
userId, err := matrix.GetUserIdFromToken(r.Context(), r.Host, accessToken)
if err != nil || userId == "" {
if err != nil {
log.Error("Error verifying token: " + err.Error())
}
return client.AuthFailed()
}
isAdmin := util.IsGlobalAdmin(userId)
if !isAdmin {
log.Warn("User " + userId + " is not a repository administrator")
return client.AuthFailed()
}

params := mux.Vars(r)

server := params["server"]
mediaId := params["mediaId"]

log = log.WithFields(logrus.Fields{
"server": server,
"mediaId": mediaId,
"userId": userId,
})

// We don't bother clearing the cache because it's still probably useful there
mediaSvc := media_service.New(r.Context(), log)
media, err := mediaSvc.GetMediaDirect(server, mediaId)
if err != nil {
log.Error("Error fetching media: " + err.Error())
return client.BadRequest("media not found or other error encountered - see logs")
}

err = mediaSvc.SetMediaQuarantined(media, true)
if err != nil {
log.Error("Error quarantining media: " + err.Error())
return client.InternalServerError("Error quarantining media")
}

return &MediaQuarantinedResponse{
IsQuarantined: true,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func main() {
previewUrlHandler := Handler{r0.PreviewUrl, hOpts}
identiconHandler := Handler{r0.Identicon, hOpts}
purgeHandler := Handler{r0.PurgeRemoteMedia, hOpts}
quarantineHandler := Handler{r0.QuarantineMedia, hOpts}

routes := make(map[string]*ApiRoute)
versions := []string{"r0", "v1"} // r0 is typically clients and v1 is typically servers
Expand All @@ -85,6 +86,7 @@ func main() {

// Custom routes for the media repo
routes["/_matrix/media/"+version+"/admin/purge_remote"] = &ApiRoute{"POST", purgeHandler}
routes["/_matrix/media/"+version+"/admin/quarantine/{server:[a-zA-Z0-9.:-_]+}/{mediaId:[a-zA-Z0-9]+}"] = &ApiRoute{"POST", quarantineHandler}

// Routes that don't fit the normal media spec
routes["/_matrix/client/"+version+"/admin/purge_media_cache"] = &ApiRoute{"POST", purgeHandler}
Expand Down
10 changes: 10 additions & 0 deletions src/github.com/turt2live/matrix-media-repo/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ type CacheConfig struct {
MinDownloads int `yaml:"minDownloads"`
}

type QuarantineConfig struct {
ReplaceThumbnails bool `yaml:"replaceThumbnails"`
ThumbnailPath string `yaml:"thumbnailPath"`
}

type MediaRepoConfig struct {
General *GeneralConfig `yaml:"repo"`
Homeservers []*HomeserverConfig `yaml:"homeservers,flow"`
Expand All @@ -99,6 +104,7 @@ type MediaRepoConfig struct {
UrlPreviews *UrlPreviewsConfig `yaml:"urlPreviews"`
RateLimit *RateLimitConfig `yaml:"rateLimit"`
Identicons *IdenticonsConfig `yaml:"identicons"`
Quarantine *QuarantineConfig `yaml:"quarantine"`
}

var instance *MediaRepoConfig
Expand Down Expand Up @@ -212,5 +218,9 @@ func NewDefaultConfig() *MediaRepoConfig {
Identicons: &IdenticonsConfig{
Enabled: true,
},
Quarantine: &QuarantineConfig{
ReplaceThumbnails: true,
ThumbnailPath: "",
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ func (c *mediaCache) GetMedia(server string, mediaId string) (*types.StreamedMed
return nil, err
}

if media.Quarantined {
c.log.Warn("Quarantined media accessed")
return nil, errs.ErrMediaQuarantined
}

// At this point we should have a real media object to use, so let's try caching it
c.incrementMediaDownloads(media)
cachedFile, err := c.updateMediaInCache(media)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import (
"io/ioutil"
"os"

"github.com/disintegration/imaging"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
"github.com/turt2live/matrix-media-repo/config"
"github.com/turt2live/matrix-media-repo/services/thumbnail_service"
"github.com/turt2live/matrix-media-repo/types"
"github.com/turt2live/matrix-media-repo/util"
"github.com/turt2live/matrix-media-repo/util/errs"
)

func (c *mediaCache) getKeyForThumbnail(server string, mediaId string, width int, height int, method string, animated bool) string {
Expand All @@ -32,6 +34,38 @@ func (c *mediaCache) GetThumbnail(server string, mediaId string, width int, heig

thumbnail, err := c.GetRawThumbnail(server, mediaId, width, height, method, animated)
if err != nil {
if err == errs.ErrMediaQuarantined {
c.log.Warn("Quarantined media accessed")
}

if err == errs.ErrMediaQuarantined && config.Get().Quarantine.ReplaceThumbnails {
c.log.Info("Replacing thumbnail with a quarantined icon")
svc := thumbnail_service.New(c.ctx, c.log)
img, err := svc.GenerateQuarantineThumbnail(server, mediaId, width, height)
if err != nil {
return nil, err
}

data := &bytes.Buffer{}
imaging.Encode(data, img, imaging.PNG)
return &types.StreamedThumbnail{
Stream: util.GetStreamFromBuffer(data),
Thumbnail: &types.Thumbnail{
// We lie about most of these details to maintain the contract
Width: img.Bounds().Max.X,
Height: img.Bounds().Max.Y,
MediaId: mediaId,
Origin: server,
Location: "",
ContentType: "image/png",
Animated: false,
Method: method,
CreationTs: util.NowMillis(),
SizeBytes: int64(data.Len()),
},
}, nil
}

return nil, err
}

Expand Down Expand Up @@ -83,6 +117,10 @@ func (c *mediaCache) GetRawThumbnail(server string, mediaId string, width int, h
return nil, err
}

if media.Quarantined {
return nil, errs.ErrMediaQuarantined
}

thumb, err := thumbnailSvc.GetThumbnailDirect(media, width, height, method, animated)
if err != nil && err != sql.ErrNoRows {
c.log.Error("Unexpected error getting thumbnail: " + err.Error())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ func (s *mediaService) IsTooLarge(contentLength int64, contentLengthHeader strin
return false // We can only assume
}

func (s *mediaService) SetMediaQuarantined(media *types.Media, isQuarantined bool) (error) {
err := s.store.SetQuarantined(media.Origin, media.MediaId, isQuarantined)
if err != nil {
return err
}

s.log.Warn("Media has been quarantined: " + media.Origin + "/" + media.MediaId)
return nil
}

func (s *mediaService) PurgeRemoteMediaBefore(beforeTs int64) (int, error) {
origins, err := s.store.GetOrigins()
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package thumbnail_service

import (
"bytes"
"context"
"errors"
"image"
"image/color"
"math"

"github.com/disintegration/imaging"
"github.com/fogleman/gg"
"github.com/golang/freetype/truetype"
"github.com/sirupsen/logrus"
"github.com/turt2live/matrix-media-repo/config"
"github.com/turt2live/matrix-media-repo/storage"
"github.com/turt2live/matrix-media-repo/storage/stores"
"github.com/turt2live/matrix-media-repo/types"
"github.com/turt2live/matrix-media-repo/util"
"github.com/turt2live/matrix-media-repo/util/errs"
"golang.org/x/image/font/gofont/gosmallcaps"
)

// These are the content types that we can actually thumbnail
Expand Down Expand Up @@ -70,3 +78,73 @@ func (s *thumbnailService) GenerateThumbnail(media *types.Media, width int, heig
result := <-getResourceHandler().GenerateThumbnail(media, width, height, method, animated, forceThumbnail)
return result.thumbnail, result.err
}

func (s *thumbnailService) GenerateQuarantineThumbnail(server string, mediaId string, 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 = s.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 (s *thumbnailService) 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)
}
Loading

0 comments on commit 4141301

Please sign in to comment.