From 599991ea731953f772824ce3ed1e591246aa004f Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 22 Jan 2018 15:32:50 -0600 Subject: PLT-3383: image proxy support (#7991) * image proxy support * go vet fix, remove mistakenly added coverage file * fix test compile error * add validation to config settings and documentation to model functions * add message_source field to post --- app/notification.go | 2 +- app/post.go | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++-- app/post_test.go | 81 ++++++++++++++++++++++++++++++++ app/reaction.go | 2 +- 4 files changed, 212 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/notification.go b/app/notification.go index 4929d56f4..62aad4c28 100644 --- a/app/notification.go +++ b/app/notification.go @@ -270,7 +270,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod } message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil) - message.Add("post", post.ToJson()) + message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) message.Add("channel_type", channel.Type) message.Add("channel_display_name", channelName) message.Add("channel_name", channel.Name) diff --git a/app/post.go b/app/post.go index 192c2effb..bf4725e77 100644 --- a/app/post.go +++ b/app/post.go @@ -4,6 +4,11 @@ package app import ( + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "encoding/base64" + "encoding/hex" "encoding/json" "fmt" "net/http" @@ -309,7 +314,7 @@ func (a *App) SendEphemeralPost(userId string, post *model.Post) *model.Post { } message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EPHEMERAL_MESSAGE, "", post.ChannelId, userId, nil) - message.Add("post", post.ToJson()) + message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) a.Go(func() { a.Publish(message) @@ -419,7 +424,7 @@ func (a *App) PatchPost(postId string, patch *model.PostPatch) (*model.Post, *mo func (a *App) sendUpdatedPostEvent(post *model.Post) { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", post.ChannelId, "", nil) - message.Add("post", post.ToJson()) + message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) a.Go(func() { a.Publish(message) @@ -562,7 +567,7 @@ func (a *App) DeletePost(postId string) (*model.Post, *model.AppError) { } message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_DELETED, "", post.ChannelId, "", nil) - message.Add("post", post.ToJson()) + message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) a.Go(func() { a.Publish(message) @@ -823,3 +828,124 @@ func (a *App) DoPostAction(postId string, actionId string, userId string) *model return nil } + +func (a *App) PostListWithProxyAddedToImageURLs(list *model.PostList) *model.PostList { + if f := a.ImageProxyAdder(); f != nil { + return list.WithRewrittenImageURLs(f) + } + return list +} + +func (a *App) PostWithProxyAddedToImageURLs(post *model.Post) *model.Post { + if f := a.ImageProxyAdder(); f != nil { + return post.WithRewrittenImageURLs(f) + } + return post +} + +func (a *App) PostWithProxyRemovedFromImageURLs(post *model.Post) *model.Post { + if f := a.ImageProxyRemover(); f != nil { + return post.WithRewrittenImageURLs(f) + } + return post +} + +func (a *App) PostPatchWithProxyRemovedFromImageURLs(patch *model.PostPatch) *model.PostPatch { + if f := a.ImageProxyRemover(); f != nil { + return patch.WithRewrittenImageURLs(f) + } + return patch +} + +func (a *App) imageProxyConfig() (proxyType, proxyURL, options, siteURL string) { + cfg := a.Config() + + if cfg.ServiceSettings.ImageProxyURL == nil || cfg.ServiceSettings.ImageProxyType == nil || cfg.ServiceSettings.SiteURL == nil { + return + } + + proxyURL = *cfg.ServiceSettings.ImageProxyURL + proxyType = *cfg.ServiceSettings.ImageProxyType + siteURL = *cfg.ServiceSettings.SiteURL + + if proxyURL == "" || proxyType == "" { + return "", "", "", "" + } + + if proxyURL[len(proxyURL)-1] != '/' { + proxyURL += "/" + } + + if cfg.ServiceSettings.ImageProxyOptions != nil { + options = *cfg.ServiceSettings.ImageProxyOptions + } + + return +} + +func (a *App) ImageProxyAdder() func(string) string { + proxyType, proxyURL, options, siteURL := a.imageProxyConfig() + if proxyType == "" { + return nil + } + + return func(url string) string { + if strings.HasPrefix(url, proxyURL) { + return url + } + + if url[0] == '/' { + url = siteURL + url + } + + switch proxyType { + case "atmos/camo": + mac := hmac.New(sha1.New, []byte(options)) + mac.Write([]byte(url)) + digest := hex.EncodeToString(mac.Sum(nil)) + return proxyURL + digest + "/" + hex.EncodeToString([]byte(url)) + case "willnorris/imageproxy": + options := strings.Split(options, "|") + if len(options) > 1 { + mac := hmac.New(sha256.New, []byte(options[1])) + mac.Write([]byte(url)) + digest := base64.URLEncoding.EncodeToString(mac.Sum(nil)) + if options[0] == "" { + return proxyURL + "s" + digest + "/" + url + } + return proxyURL + options[0] + ",s" + digest + "/" + url + } + return proxyURL + options[0] + "/" + url + } + + return url + } +} + +func (a *App) ImageProxyRemover() (f func(string) string) { + proxyType, proxyURL, _, _ := a.imageProxyConfig() + if proxyType == "" { + return nil + } + + return func(url string) string { + switch proxyType { + case "atmos/camo": + if strings.HasPrefix(url, proxyURL) { + if slash := strings.IndexByte(url[len(proxyURL):], '/'); slash >= 0 { + if decoded, err := hex.DecodeString(url[len(proxyURL)+slash+1:]); err == nil { + return string(decoded) + } + } + } + case "willnorris/imageproxy": + if strings.HasPrefix(url, proxyURL) { + if slash := strings.IndexByte(url[len(proxyURL):], '/'); slash >= 0 { + return url[len(proxyURL)+slash+1:] + } + } + } + + return url + } +} diff --git a/app/post_test.go b/app/post_test.go index 82eac3cd1..9854bb707 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -185,3 +185,84 @@ func TestPostChannelMentions(t *testing.T) { }, }, result.Props["channel_mentions"]) } + +func TestImageProxy(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + for name, tc := range map[string]struct { + ProxyType string + ProxyURL string + ProxyOptions string + ImageURL string + ProxiedImageURL string + }{ + "atmos/camo": { + ProxyType: "atmos/camo", + ProxyURL: "https://127.0.0.1", + ProxyOptions: "foo", + ImageURL: "http://mydomain.com/myimage", + ProxiedImageURL: "https://127.0.0.1/f8dace906d23689e8d5b12c3cefbedbf7b9b72f5/687474703a2f2f6d79646f6d61696e2e636f6d2f6d79696d616765", + }, + "willnorris/imageproxy": { + ProxyType: "willnorris/imageproxy", + ProxyURL: "https://127.0.0.1", + ProxyOptions: "x1000", + ImageURL: "http://mydomain.com/myimage", + ProxiedImageURL: "https://127.0.0.1/x1000/http://mydomain.com/myimage", + }, + "willnorris/imageproxy_WithSigning": { + ProxyType: "willnorris/imageproxy", + ProxyURL: "https://127.0.0.1", + ProxyOptions: "x1000|foo", + ImageURL: "http://mydomain.com/myimage", + ProxiedImageURL: "https://127.0.0.1/x1000,sbhHVoG5d60UvnNtGh6Iy6x4PaMmnsh8JfZ7JfErKjGU=/http://mydomain.com/myimage", + }, + } { + t.Run(name, func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.ServiceSettings.ImageProxyType = model.NewString(tc.ProxyType) + cfg.ServiceSettings.ImageProxyOptions = model.NewString(tc.ProxyOptions) + cfg.ServiceSettings.ImageProxyURL = model.NewString(tc.ProxyURL) + }) + + post := &model.Post{ + Id: model.NewId(), + Message: "![foo](" + tc.ImageURL + ")", + } + + list := model.NewPostList() + list.Posts[post.Id] = post + + assert.Equal(t, "![foo]("+tc.ProxiedImageURL+")", th.App.PostListWithProxyAddedToImageURLs(list).Posts[post.Id].Message) + assert.Equal(t, "![foo]("+tc.ProxiedImageURL+")", th.App.PostWithProxyAddedToImageURLs(post).Message) + + assert.Equal(t, "![foo]("+tc.ImageURL+")", th.App.PostWithProxyRemovedFromImageURLs(post).Message) + post.Message = "![foo](" + tc.ProxiedImageURL + ")" + assert.Equal(t, "![foo]("+tc.ImageURL+")", th.App.PostWithProxyRemovedFromImageURLs(post).Message) + }) + } +} + +var imageProxyBenchmarkSink *model.Post + +func BenchmarkPostWithProxyRemovedFromImageURLs(b *testing.B) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.ServiceSettings.ImageProxyType = model.NewString("willnorris/imageproxy") + cfg.ServiceSettings.ImageProxyOptions = model.NewString("x1000|foo") + cfg.ServiceSettings.ImageProxyURL = model.NewString("https://127.0.0.1") + }) + + post := &model.Post{ + Message: "![foo](http://mydomain.com/myimage)", + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + imageProxyBenchmarkSink = th.App.PostWithProxyAddedToImageURLs(post) + } +} diff --git a/app/reaction.go b/app/reaction.go index bf0d20e2b..062622f34 100644 --- a/app/reaction.go +++ b/app/reaction.go @@ -62,6 +62,6 @@ func (a *App) sendReactionEvent(event string, reaction *model.Reaction, post *mo post.HasReactions = true post.UpdateAt = model.GetMillis() umessage := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", post.ChannelId, "", nil) - umessage.Add("post", post.ToJson()) + umessage.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) a.Publish(umessage) } -- cgit v1.2.3-1-g7c22