mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-31 01:11:29 +08:00
Remove atmos/camo image proxy support (#37284)
This commit is contained in:
@@ -775,8 +775,6 @@ const defaultServerConfig: AdminConfig = {
|
||||
ImageProxySettings: {
|
||||
Enable: false,
|
||||
ImageProxyType: 'local',
|
||||
RemoteImageProxyURL: '',
|
||||
RemoteImageProxyOptions: '',
|
||||
},
|
||||
CloudSettings: {
|
||||
CWSURL: 'https://customers.mattermost.com',
|
||||
|
||||
@@ -215,7 +215,6 @@ linters:
|
||||
platform/services/cache/lru_test.go|\
|
||||
platform/services/docextractor/combine.go|\
|
||||
platform/services/docextractor/pdf.go|\
|
||||
platform/services/imageproxy/atmos_camo_test.go|\
|
||||
platform/services/imageproxy/local.go|\
|
||||
platform/services/imageproxy/local_test.go|\
|
||||
platform/services/remotecluster/invitation.go|\
|
||||
|
||||
@@ -43,27 +43,6 @@ func TestGetImage(t *testing.T) {
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("atmos/camo", func(t *testing.T) {
|
||||
imageURL := "http://foo.bar/baz.gif"
|
||||
proxiedURL := "https://proxy.foo.bar/83d4d9ac78b76ce425ea67038826df867c62cc5c/687474703a2f2f666f6f2e6261722f62617a2e676966"
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ImageProxySettings.Enable = new(true)
|
||||
cfg.ImageProxySettings.ImageProxyType = new("atmos/camo")
|
||||
cfg.ImageProxySettings.RemoteImageProxyOptions = new("7e5f3fab20b94782b43cdb022a66985ef28ba355df2c5d5da3c9a05e4b697bac")
|
||||
cfg.ImageProxySettings.RemoteImageProxyURL = new("https://proxy.foo.bar")
|
||||
})
|
||||
|
||||
r, err := http.NewRequest("GET", th.Client.APIURL+"/image?url="+url.QueryEscape(imageURL), nil)
|
||||
require.NoError(t, err)
|
||||
r.Header.Set(model.HeaderAuth, th.Client.AuthType+" "+th.Client.AuthToken)
|
||||
|
||||
resp, err := th.Client.HTTPClient.Do(r)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusFound, resp.StatusCode)
|
||||
assert.Equal(t, proxiedURL, resp.Header.Get("Location"))
|
||||
})
|
||||
|
||||
t.Run("local", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ImageProxySettings.Enable = new(true)
|
||||
|
||||
@@ -43,9 +43,7 @@ func (s *Server) initPostMetadata() {
|
||||
// Dump any cached links if the proxy settings have changed so image URLs can be updated
|
||||
s.platform.AddConfigListener(func(before, after *model.Config) {
|
||||
if (before.ImageProxySettings.Enable != after.ImageProxySettings.Enable) ||
|
||||
(before.ImageProxySettings.ImageProxyType != after.ImageProxySettings.ImageProxyType) ||
|
||||
(before.ImageProxySettings.RemoteImageProxyURL != after.ImageProxySettings.RemoteImageProxyURL) ||
|
||||
(before.ImageProxySettings.RemoteImageProxyOptions != after.ImageProxySettings.RemoteImageProxyOptions) {
|
||||
(before.ImageProxySettings.ImageProxyType != after.ImageProxySettings.ImageProxyType) {
|
||||
if err := platform.PurgeLinkCache(); err != nil {
|
||||
mlog.Warn("Failed to remove cached links when the proxy settings changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
@@ -1035,9 +1035,7 @@ func TestPreparePostForClientWithImageProxy(t *testing.T) {
|
||||
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
||||
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
|
||||
*cfg.ImageProxySettings.Enable = true
|
||||
*cfg.ImageProxySettings.ImageProxyType = "atmos/camo"
|
||||
*cfg.ImageProxySettings.RemoteImageProxyURL = "https://127.0.0.1"
|
||||
*cfg.ImageProxySettings.RemoteImageProxyOptions = model.NewTestPassword()
|
||||
*cfg.ImageProxySettings.ImageProxyType = "local"
|
||||
})
|
||||
|
||||
th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log())
|
||||
|
||||
@@ -894,48 +894,12 @@ func TestImageProxy(t *testing.T) {
|
||||
|
||||
th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log())
|
||||
|
||||
testHMACKey := model.NewTestPassword()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
ProxyType string
|
||||
ProxyURL string
|
||||
ProxyOptions string
|
||||
ImageURL string
|
||||
ProxiedImageURL string
|
||||
ProxiedRemovedImageURL string
|
||||
}{
|
||||
"atmos/camo": {
|
||||
ProxyType: model.ImageProxyTypeAtmosCamo,
|
||||
ProxyURL: "https://127.0.0.1",
|
||||
ProxyOptions: testHMACKey,
|
||||
ImageURL: "http://mydomain.com/myimage",
|
||||
ProxiedRemovedImageURL: "http://mydomain.com/myimage",
|
||||
ProxiedImageURL: "http://mymattermost.com/api/v4/image?url=http%3A%2F%2Fmydomain.com%2Fmyimage",
|
||||
},
|
||||
"atmos/camo_SameSite": {
|
||||
ProxyType: model.ImageProxyTypeAtmosCamo,
|
||||
ProxyURL: "https://127.0.0.1",
|
||||
ProxyOptions: testHMACKey,
|
||||
ImageURL: "http://mymattermost.com/myimage",
|
||||
ProxiedRemovedImageURL: "http://mymattermost.com/myimage",
|
||||
ProxiedImageURL: "http://mymattermost.com/myimage",
|
||||
},
|
||||
"atmos/camo_PathOnly": {
|
||||
ProxyType: model.ImageProxyTypeAtmosCamo,
|
||||
ProxyURL: "https://127.0.0.1",
|
||||
ProxyOptions: testHMACKey,
|
||||
ImageURL: "/myimage",
|
||||
ProxiedRemovedImageURL: "http://mymattermost.com/myimage",
|
||||
ProxiedImageURL: "http://mymattermost.com/myimage",
|
||||
},
|
||||
"atmos/camo_EmptyImageURL": {
|
||||
ProxyType: model.ImageProxyTypeAtmosCamo,
|
||||
ProxyURL: "https://127.0.0.1",
|
||||
ProxyOptions: testHMACKey,
|
||||
ImageURL: "",
|
||||
ProxiedRemovedImageURL: "",
|
||||
ProxiedImageURL: "",
|
||||
},
|
||||
"local": {
|
||||
ProxyType: model.ImageProxyTypeLocal,
|
||||
ImageURL: "http://mydomain.com/myimage",
|
||||
@@ -965,8 +929,6 @@ func TestImageProxy(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ImageProxySettings.Enable = new(true)
|
||||
cfg.ImageProxySettings.ImageProxyType = new(tc.ProxyType)
|
||||
cfg.ImageProxySettings.RemoteImageProxyOptions = new(tc.ProxyOptions)
|
||||
cfg.ImageProxySettings.RemoteImageProxyURL = new(tc.ProxyURL)
|
||||
})
|
||||
|
||||
post := &model.Post{
|
||||
@@ -1164,9 +1126,7 @@ func TestCreatePost(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
||||
*cfg.ImageProxySettings.Enable = true
|
||||
*cfg.ImageProxySettings.ImageProxyType = "atmos/camo"
|
||||
*cfg.ImageProxySettings.RemoteImageProxyURL = "https://127.0.0.1"
|
||||
*cfg.ImageProxySettings.RemoteImageProxyOptions = model.NewTestPassword()
|
||||
*cfg.ImageProxySettings.ImageProxyType = "local"
|
||||
})
|
||||
|
||||
th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log())
|
||||
@@ -2029,9 +1989,7 @@ func TestPatchPost(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
||||
*cfg.ImageProxySettings.Enable = true
|
||||
*cfg.ImageProxySettings.ImageProxyType = "atmos/camo"
|
||||
*cfg.ImageProxySettings.RemoteImageProxyURL = "https://127.0.0.1"
|
||||
*cfg.ImageProxySettings.RemoteImageProxyOptions = model.NewTestPassword()
|
||||
*cfg.ImageProxySettings.ImageProxyType = "local"
|
||||
})
|
||||
|
||||
th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log())
|
||||
@@ -2490,9 +2448,7 @@ func TestUpdatePost(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.SiteURL = "http://mymattermost.com"
|
||||
*cfg.ImageProxySettings.Enable = true
|
||||
*cfg.ImageProxySettings.ImageProxyType = "atmos/camo"
|
||||
*cfg.ImageProxySettings.RemoteImageProxyURL = "https://127.0.0.1"
|
||||
*cfg.ImageProxySettings.RemoteImageProxyOptions = model.NewTestPassword()
|
||||
*cfg.ImageProxySettings.ImageProxyType = "local"
|
||||
})
|
||||
|
||||
th.App.ch.imageProxy = imageproxy.MakeImageProxy(th.Server.platform, th.Server.HTTPService(), th.Server.Log())
|
||||
|
||||
@@ -598,9 +598,7 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
// Dump the image cache if the proxy settings have changed. (need switch URLs to the correct proxy)
|
||||
s.platform.AddConfigListener(func(oldCfg, newCfg *model.Config) {
|
||||
if (oldCfg.ImageProxySettings.Enable != newCfg.ImageProxySettings.Enable) ||
|
||||
(oldCfg.ImageProxySettings.ImageProxyType != newCfg.ImageProxySettings.ImageProxyType) ||
|
||||
(oldCfg.ImageProxySettings.RemoteImageProxyURL != newCfg.ImageProxySettings.RemoteImageProxyURL) ||
|
||||
(oldCfg.ImageProxySettings.RemoteImageProxyOptions != newCfg.ImageProxySettings.RemoteImageProxyOptions) {
|
||||
(oldCfg.ImageProxySettings.ImageProxyType != newCfg.ImageProxySettings.ImageProxyType) {
|
||||
if err = s.openGraphDataCache.Purge(); err != nil {
|
||||
mlog.Error("Failed to purge Open Graph data cache after config change", mlog.Err(err))
|
||||
}
|
||||
|
||||
+3
-11
@@ -11719,16 +11719,8 @@
|
||||
"translation": "Invalid timeout value {{.Value}}. Should be a positive number."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.atmos_camo_image_proxy_options.app_error",
|
||||
"translation": "Invalid RemoteImageProxyOptions for atmos/camo. Must be set to your shared key."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.atmos_camo_image_proxy_options_length.app_error",
|
||||
"translation": "Invalid RemoteImageProxyOptions for atmos/camo: HMAC key must be at least {{.MinLength}} bytes for FIPS compliance."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.atmos_camo_image_proxy_url.app_error",
|
||||
"translation": "Invalid RemoteImageProxyURL for atmos/camo. Must be set to your shared key."
|
||||
"id": "model.config.is_valid.atmos_camo_image_proxy_removed.app_error",
|
||||
"translation": "The 'atmos/camo' image proxy type was removed in Mattermost v12.0. Set ImageProxySettings.ImageProxyType to 'local', or set ImageProxySettings.Enable to false to disable the image proxy."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.autotranslation.agents.llm_service_id.app_error",
|
||||
@@ -11992,7 +11984,7 @@
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.image_proxy_type.app_error",
|
||||
"translation": "Invalid image proxy type. Must be 'local' or 'atmos/camo'."
|
||||
"translation": "Invalid image proxy type. Must be 'local'."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.import.directory.app_error",
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package imageproxy
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
type AtmosCamoBackend struct {
|
||||
siteURL *url.URL
|
||||
remoteOptions string
|
||||
remoteURL *url.URL
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func makeAtmosCamoBackend(proxy *ImageProxy, proxySettings model.ImageProxySettings) *AtmosCamoBackend {
|
||||
// We deliberately ignore the error because it's from config.json.
|
||||
// The function returns a nil pointer in case of error, and we handle it when it's used.
|
||||
remoteURL, _ := url.Parse(*proxySettings.RemoteImageProxyURL)
|
||||
|
||||
return &AtmosCamoBackend{
|
||||
siteURL: proxy.siteURL,
|
||||
remoteURL: remoteURL,
|
||||
remoteOptions: *proxySettings.RemoteImageProxyOptions,
|
||||
client: proxy.HTTPService.MakeClient(false),
|
||||
}
|
||||
}
|
||||
|
||||
func (backend *AtmosCamoBackend) GetImage(w http.ResponseWriter, r *http.Request, imageURL string) {
|
||||
http.Redirect(w, r, backend.getAtmosCamoImageURL(imageURL), http.StatusFound)
|
||||
}
|
||||
|
||||
func (backend *AtmosCamoBackend) GetImageDirect(imageURL string) (io.ReadCloser, string, error) {
|
||||
req, err := http.NewRequest("GET", backend.getAtmosCamoImageURL(imageURL), nil)
|
||||
if err != nil {
|
||||
return nil, "", Error{err}
|
||||
}
|
||||
|
||||
resp, err := backend.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, "", Error{err}
|
||||
}
|
||||
|
||||
// Note that we don't do any additional validation of the received data since we expect the image proxy to do that
|
||||
return resp.Body, resp.Header.Get("Content-Type"), nil
|
||||
}
|
||||
|
||||
func (backend *AtmosCamoBackend) getAtmosCamoImageURL(imageURL string) string {
|
||||
if imageURL == "" || backend.siteURL == nil {
|
||||
return imageURL
|
||||
}
|
||||
|
||||
// Parse url, return siteURL in case of failure.
|
||||
// Also if the URL is opaque.
|
||||
parsedURL, err := url.Parse(imageURL)
|
||||
if err != nil || parsedURL.Opaque != "" {
|
||||
return backend.siteURL.String()
|
||||
}
|
||||
|
||||
// If host is same as siteURL host/ remoteURL host, return.
|
||||
if parsedURL.Host == backend.siteURL.Host || parsedURL.Host == backend.remoteURL.Host {
|
||||
return parsedURL.String()
|
||||
}
|
||||
|
||||
// Handle protocol-relative URLs.
|
||||
if parsedURL.Scheme == "" {
|
||||
parsedURL.Scheme = backend.siteURL.Scheme
|
||||
}
|
||||
|
||||
// If it's a relative URL, fill up the hostname and scheme and return.
|
||||
if parsedURL.Host == "" {
|
||||
parsedURL.Host = backend.siteURL.Host
|
||||
return parsedURL.String()
|
||||
}
|
||||
|
||||
urlBytes := []byte(parsedURL.String())
|
||||
mac := hmac.New(sha1.New, []byte(backend.remoteOptions))
|
||||
mac.Write(urlBytes)
|
||||
digest := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
return backend.remoteURL.String() + "/" + digest + "/" + hex.EncodeToString(urlBytes)
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package imageproxy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/httpservice"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
|
||||
)
|
||||
|
||||
func makeTestAtmosCamoProxy() *ImageProxy {
|
||||
configService := &testutils.StaticConfigService{
|
||||
Cfg: &model.Config{
|
||||
ServiceSettings: model.ServiceSettings{
|
||||
SiteURL: new("https://mattermost.example.com"),
|
||||
AllowedUntrustedInternalConnections: new("127.0.0.1"),
|
||||
},
|
||||
ImageProxySettings: model.ImageProxySettings{
|
||||
Enable: new(true),
|
||||
ImageProxyType: model.NewPointer(model.ImageProxyTypeAtmosCamo),
|
||||
RemoteImageProxyURL: new("http://images.example.com"),
|
||||
RemoteImageProxyOptions: new("7e5f3fab20b94782b43cdb022a66985ef28ba355df2c5d5da3c9a05e4b697bac"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return MakeImageProxy(configService, httpservice.MakeHTTPService(configService), nil)
|
||||
}
|
||||
|
||||
func TestAtmosCamoBackend_GetImage(t *testing.T) {
|
||||
imageURL := "https://www.mattermost.com/wp-content/uploads/2022/02/logoHorizontalWhite.png"
|
||||
proxiedURL := "http://images.example.com/b569ce17f1be4550cffa8d8dd3a9e80e6d209584/68747470733a2f2f7777772e6d61747465726d6f73742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323032322f30322f6c6f676f486f72697a6f6e74616c57686974652e706e67"
|
||||
|
||||
proxy := makeTestAtmosCamoProxy()
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request, _ := http.NewRequest(http.MethodGet, "", nil)
|
||||
proxy.GetImage(recorder, request, imageURL)
|
||||
resp := recorder.Result()
|
||||
|
||||
assert.Equal(t, http.StatusFound, resp.StatusCode)
|
||||
assert.Equal(t, proxiedURL, resp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func TestAtmosCamoBackend_GetImageDirect(t *testing.T) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "max-age=2592000, private")
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Header().Set("Content-Length", "10")
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("1111111111"))
|
||||
})
|
||||
|
||||
mock := httptest.NewServer(handler)
|
||||
defer mock.Close()
|
||||
|
||||
proxy := makeTestAtmosCamoProxy()
|
||||
parsedURL, err := url.Parse("https://mattermost.example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
remoteURL, err := url.Parse(mock.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
backend := &AtmosCamoBackend{
|
||||
siteURL: parsedURL,
|
||||
remoteURL: remoteURL,
|
||||
client: proxy.HTTPService.MakeClient(false),
|
||||
}
|
||||
|
||||
body, contentType, err := backend.GetImageDirect("https://example.com/image.png")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "image/png", contentType)
|
||||
|
||||
require.NotNil(t, body)
|
||||
respBody, _ := io.ReadAll(body)
|
||||
assert.Equal(t, []byte("1111111111"), respBody)
|
||||
}
|
||||
|
||||
func TestGetAtmosCamoImageURL(t *testing.T) {
|
||||
imageURL := "https://mattermost.com/wp-content/uploads/2022/02/logoHorizontal.png"
|
||||
proxiedURL := "http://images.example.com/03b122734ae088d10cb46ea05512ec7dc852299e/68747470733a2f2f6d61747465726d6f73742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323032322f30322f6c6f676f486f72697a6f6e74616c2e706e67"
|
||||
|
||||
defaultSiteURL := "https://mattermost.example.com"
|
||||
proxyURL := "http://images.example.com"
|
||||
|
||||
for _, test := range []struct {
|
||||
Name string
|
||||
Input string
|
||||
SiteURL string
|
||||
Expected string
|
||||
}{
|
||||
{
|
||||
Name: "should proxy image",
|
||||
Input: imageURL,
|
||||
SiteURL: defaultSiteURL,
|
||||
Expected: proxiedURL,
|
||||
},
|
||||
{
|
||||
Name: "should proxy image when no site URL is set",
|
||||
Input: imageURL,
|
||||
SiteURL: "",
|
||||
Expected: proxiedURL,
|
||||
},
|
||||
{
|
||||
Name: "should proxy image when a site URL with a subpath is set",
|
||||
Input: imageURL,
|
||||
SiteURL: proxyURL + "/subpath",
|
||||
Expected: proxiedURL,
|
||||
},
|
||||
{
|
||||
Name: "should not proxy a relative image",
|
||||
Input: "/static/logo.png",
|
||||
SiteURL: defaultSiteURL,
|
||||
Expected: "https://mattermost.example.com/static/logo.png",
|
||||
},
|
||||
{
|
||||
Name: "should bypass opaque URLs",
|
||||
Input: "http:xyz123?query",
|
||||
SiteURL: defaultSiteURL,
|
||||
Expected: defaultSiteURL,
|
||||
},
|
||||
{
|
||||
Name: "should not proxy an image on the Mattermost server",
|
||||
Input: "https://mattermost.example.com/static/logo.png",
|
||||
SiteURL: defaultSiteURL,
|
||||
Expected: "https://mattermost.example.com/static/logo.png",
|
||||
},
|
||||
{
|
||||
Name: "should not proxy an image on the Mattermost server when a subpath is set",
|
||||
Input: "https://mattermost.example.com/static/logo.png",
|
||||
SiteURL: defaultSiteURL + "/static",
|
||||
Expected: "https://mattermost.example.com/static/logo.png",
|
||||
},
|
||||
{
|
||||
Name: "should not proxy an image that has already been proxied",
|
||||
Input: proxiedURL,
|
||||
SiteURL: defaultSiteURL,
|
||||
Expected: proxiedURL,
|
||||
},
|
||||
{
|
||||
Name: "should not bypass protocol relative URLs",
|
||||
Input: "https://mattermost.com/wp-content/uploads/2022/02/logoHorizontal.png",
|
||||
SiteURL: "http://mattermost.example.com",
|
||||
Expected: proxiedURL,
|
||||
},
|
||||
{
|
||||
Name: "should not bypass if the host prefix is same",
|
||||
Input: "https://mattermost.com/wp-content/uploads/2022/02/logoHorizontal.png",
|
||||
SiteURL: defaultSiteURL,
|
||||
Expected: "http://images.example.com/03b122734ae088d10cb46ea05512ec7dc852299e/68747470733a2f2f6d61747465726d6f73742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323032322f30322f6c6f676f486f72697a6f6e74616c2e706e67",
|
||||
},
|
||||
{
|
||||
Name: "should not bypass for user auth URLs",
|
||||
Input: "https://mattermost.com/wp-content/uploads/2022/02/logoHorizontal.png",
|
||||
SiteURL: defaultSiteURL,
|
||||
Expected: "http://images.example.com/03b122734ae088d10cb46ea05512ec7dc852299e/68747470733a2f2f6d61747465726d6f73742e636f6d2f77702d636f6e74656e742f75706c6f6164732f323032322f30322f6c6f676f486f72697a6f6e74616c2e706e67",
|
||||
},
|
||||
} {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
parsedURL, err := url.Parse(test.SiteURL)
|
||||
require.NoError(t, err)
|
||||
|
||||
remoteURL, err := url.Parse(proxyURL)
|
||||
require.NoError(t, err)
|
||||
|
||||
backend := &AtmosCamoBackend{
|
||||
siteURL: parsedURL,
|
||||
remoteURL: remoteURL,
|
||||
remoteOptions: *makeTestAtmosCamoProxy().ConfigService.Config().ImageProxySettings.RemoteImageProxyOptions,
|
||||
}
|
||||
|
||||
assert.Equal(t, test.Expected, backend.getAtmosCamoImageURL(test.Input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -73,8 +73,6 @@ func (proxy *ImageProxy) makeBackend(proxySettings model.ImageProxySettings) Ima
|
||||
switch *proxySettings.ImageProxyType {
|
||||
case model.ImageProxyTypeLocal:
|
||||
return makeLocalBackend(proxy)
|
||||
case model.ImageProxyTypeAtmosCamo:
|
||||
return makeAtmosCamoBackend(proxy, proxySettings)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -119,26 +118,6 @@ func TestGetUnproxiedImageURL(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOnConfigChange(t *testing.T) {
|
||||
t.Run("should switch between backends", func(t *testing.T) {
|
||||
proxy := makeTestAtmosCamoProxy()
|
||||
|
||||
require.Equal(t, "https://mattermost.example.com", proxy.backend.(*AtmosCamoBackend).siteURL.String())
|
||||
|
||||
newConfig := proxy.ConfigService.Config().Clone()
|
||||
newConfig.ImageProxySettings.ImageProxyType = model.NewPointer(model.ImageProxyTypeLocal)
|
||||
|
||||
proxy.ConfigService.(*testutils.StaticConfigService).UpdateConfig(newConfig)
|
||||
|
||||
require.Equal(t, "https://mattermost.example.com", proxy.backend.(*LocalBackend).baseURL.String())
|
||||
|
||||
newConfig = proxy.ConfigService.Config().Clone()
|
||||
newConfig.ImageProxySettings.ImageProxyType = model.NewPointer(model.ImageProxyTypeAtmosCamo)
|
||||
|
||||
proxy.ConfigService.(*testutils.StaticConfigService).UpdateConfig(newConfig)
|
||||
|
||||
require.Equal(t, "https://mattermost.example.com", proxy.backend.(*AtmosCamoBackend).siteURL.String())
|
||||
})
|
||||
|
||||
t.Run("for local proxy, should update site URL when that changes", func(t *testing.T) {
|
||||
proxy := makeTestLocalProxy()
|
||||
|
||||
@@ -153,35 +132,4 @@ func TestOnConfigChange(t *testing.T) {
|
||||
require.Equal(t, "https://new.example.com", proxy.siteURL.String())
|
||||
require.Equal(t, "https://new.example.com", proxy.backend.(*LocalBackend).baseURL.String())
|
||||
})
|
||||
|
||||
t.Run("for atmos/camo proxy, should update site URL when that changes", func(t *testing.T) {
|
||||
proxy := makeTestAtmosCamoProxy()
|
||||
|
||||
require.Equal(t, "https://mattermost.example.com", proxy.siteURL.String())
|
||||
require.Equal(t, "https://mattermost.example.com", proxy.backend.(*AtmosCamoBackend).siteURL.String())
|
||||
|
||||
newConfig := proxy.ConfigService.Config().Clone()
|
||||
newConfig.ServiceSettings.SiteURL = new("https://new.example.com")
|
||||
|
||||
proxy.ConfigService.(*testutils.StaticConfigService).UpdateConfig(newConfig)
|
||||
|
||||
require.Equal(t, "https://new.example.com", proxy.siteURL.String())
|
||||
require.Equal(t, "https://new.example.com", proxy.backend.(*AtmosCamoBackend).siteURL.String())
|
||||
})
|
||||
|
||||
t.Run("for atmos/camo proxy, should update additional options when those change", func(t *testing.T) {
|
||||
proxy := makeTestAtmosCamoProxy()
|
||||
|
||||
require.Equal(t, "http://images.example.com", proxy.backend.(*AtmosCamoBackend).remoteURL.String())
|
||||
// require.Equal(t, "7e5f3fab20b94782b43cdb022a66985ef28ba355df2c5d5da3c9a05e4b697bac", proxy.backend.(*AtmosCamoBackend).remoteOptions)
|
||||
|
||||
newConfig := proxy.ConfigService.Config().Clone()
|
||||
newConfig.ImageProxySettings.RemoteImageProxyURL = new("https://new.example.com")
|
||||
newConfig.ImageProxySettings.RemoteImageProxyOptions = new("some other random hash")
|
||||
|
||||
proxy.ConfigService.(*testutils.StaticConfigService).UpdateConfig(newConfig)
|
||||
|
||||
require.Equal(t, "https://new.example.com", proxy.backend.(*AtmosCamoBackend).remoteURL.String())
|
||||
// require.Equal(t, "some other random hash", proxy.backend.(*AtmosCamoBackend).remoteOptions)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -286,8 +286,8 @@ const (
|
||||
GlobalrelayCustomerTypeA10 = "A10"
|
||||
GlobalrelayCustomerTypeCustom = "CUSTOM"
|
||||
|
||||
ImageProxyTypeLocal = "local"
|
||||
ImageProxyTypeAtmosCamo = "atmos/camo"
|
||||
ImageProxyTypeLocal = "local"
|
||||
ImageProxyTypeLegacyAtmosCamo = "atmos/camo"
|
||||
|
||||
GoogleSettingsDefaultScope = "profile email"
|
||||
GoogleSettingsDefaultAuthEndpoint = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
@@ -3986,10 +3986,8 @@ func (s *GuestAccountsSettings) IsValid() *AppError {
|
||||
}
|
||||
|
||||
type ImageProxySettings struct {
|
||||
Enable *bool `access:"environment_image_proxy"`
|
||||
ImageProxyType *string `access:"environment_image_proxy"`
|
||||
RemoteImageProxyURL *string `access:"environment_image_proxy"`
|
||||
RemoteImageProxyOptions *string `access:"environment_image_proxy"`
|
||||
Enable *bool `access:"environment_image_proxy"`
|
||||
ImageProxyType *string `access:"environment_image_proxy"`
|
||||
}
|
||||
|
||||
func (s *ImageProxySettings) SetDefaults() {
|
||||
@@ -4000,14 +3998,6 @@ func (s *ImageProxySettings) SetDefaults() {
|
||||
if s.ImageProxyType == nil {
|
||||
s.ImageProxyType = new(ImageProxyTypeLocal)
|
||||
}
|
||||
|
||||
if s.RemoteImageProxyURL == nil {
|
||||
s.RemoteImageProxyURL = new("")
|
||||
}
|
||||
|
||||
if s.RemoteImageProxyOptions == nil {
|
||||
s.RemoteImageProxyOptions = new("")
|
||||
}
|
||||
}
|
||||
|
||||
// ImportSettings defines configuration settings for file imports.
|
||||
@@ -5295,24 +5285,14 @@ func (s *DisplaySettings) isValid() *AppError {
|
||||
}
|
||||
|
||||
func (s *ImageProxySettings) isValid() *AppError {
|
||||
if *s.ImageProxyType == ImageProxyTypeLegacyAtmosCamo {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.atmos_camo_image_proxy_removed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if *s.Enable {
|
||||
switch *s.ImageProxyType {
|
||||
case ImageProxyTypeLocal:
|
||||
// No other settings to validate
|
||||
case ImageProxyTypeAtmosCamo:
|
||||
if *s.RemoteImageProxyURL == "" {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.atmos_camo_image_proxy_url.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if *s.RemoteImageProxyOptions == "" {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.atmos_camo_image_proxy_options.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// RemoteImageProxyOptions is used as the HMAC key for URL signing,
|
||||
// so it is subject to the same FIPS minimum key length as passwords.
|
||||
if FIPSEnabled && len(*s.RemoteImageProxyOptions) < PasswordFIPSMinimumLength {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.atmos_camo_image_proxy_options_length.app_error", map[string]any{"MinLength": PasswordFIPSMinimumLength}, "", http.StatusBadRequest)
|
||||
}
|
||||
default:
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.image_proxy_type.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -1354,21 +1354,15 @@ func TestImageProxySettingsSetDefaults(t *testing.T) {
|
||||
|
||||
assert.Equal(t, false, *ips.Enable)
|
||||
assert.Equal(t, ImageProxyTypeLocal, *ips.ImageProxyType)
|
||||
assert.Equal(t, "", *ips.RemoteImageProxyURL)
|
||||
assert.Equal(t, "", *ips.RemoteImageProxyOptions)
|
||||
})
|
||||
}
|
||||
|
||||
func TestImageProxySettingsIsValid(t *testing.T) {
|
||||
testHMACKey := NewTestPassword()
|
||||
|
||||
for _, test := range []struct {
|
||||
Name string
|
||||
Enable bool
|
||||
ImageProxyType string
|
||||
RemoteImageProxyURL string
|
||||
RemoteImageProxyOptions string
|
||||
ExpectError bool
|
||||
Name string
|
||||
Enable bool
|
||||
ImageProxyType string
|
||||
ExpectError bool
|
||||
}{
|
||||
{
|
||||
Name: "disabled",
|
||||
@@ -1376,12 +1370,22 @@ func TestImageProxySettingsIsValid(t *testing.T) {
|
||||
ExpectError: false,
|
||||
},
|
||||
{
|
||||
Name: "disabled with bad values",
|
||||
Enable: false,
|
||||
ImageProxyType: "garbage",
|
||||
RemoteImageProxyURL: "garbage",
|
||||
RemoteImageProxyOptions: "garbage",
|
||||
ExpectError: false,
|
||||
Name: "disabled with bad values",
|
||||
Enable: false,
|
||||
ImageProxyType: "garbage",
|
||||
ExpectError: false,
|
||||
},
|
||||
{
|
||||
Name: "atmos/camo, disabled",
|
||||
Enable: false,
|
||||
ImageProxyType: ImageProxyTypeLegacyAtmosCamo,
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "atmos/camo, enabled",
|
||||
Enable: true,
|
||||
ImageProxyType: ImageProxyTypeLegacyAtmosCamo,
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "missing type",
|
||||
@@ -1390,52 +1394,16 @@ func TestImageProxySettingsIsValid(t *testing.T) {
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "local",
|
||||
Enable: true,
|
||||
ImageProxyType: "local",
|
||||
RemoteImageProxyURL: "garbage",
|
||||
RemoteImageProxyOptions: "garbage",
|
||||
ExpectError: false,
|
||||
},
|
||||
{
|
||||
Name: "atmos/camo",
|
||||
Enable: true,
|
||||
ImageProxyType: ImageProxyTypeAtmosCamo,
|
||||
RemoteImageProxyURL: "someurl",
|
||||
RemoteImageProxyOptions: testHMACKey,
|
||||
ExpectError: false,
|
||||
},
|
||||
{
|
||||
Name: "atmos/camo, missing url",
|
||||
Enable: true,
|
||||
ImageProxyType: ImageProxyTypeAtmosCamo,
|
||||
RemoteImageProxyURL: "",
|
||||
RemoteImageProxyOptions: "garbage",
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "atmos/camo, missing options",
|
||||
Enable: true,
|
||||
ImageProxyType: ImageProxyTypeAtmosCamo,
|
||||
RemoteImageProxyURL: "someurl",
|
||||
RemoteImageProxyOptions: "",
|
||||
ExpectError: true,
|
||||
},
|
||||
{
|
||||
Name: "atmos/camo, short options under FIPS",
|
||||
Enable: true,
|
||||
ImageProxyType: ImageProxyTypeAtmosCamo,
|
||||
RemoteImageProxyURL: "someurl",
|
||||
RemoteImageProxyOptions: "foo",
|
||||
ExpectError: FIPSEnabled,
|
||||
Name: "local",
|
||||
Enable: true,
|
||||
ImageProxyType: "local",
|
||||
ExpectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
ips := &ImageProxySettings{
|
||||
Enable: &test.Enable,
|
||||
ImageProxyType: &test.ImageProxyType,
|
||||
RemoteImageProxyURL: &test.RemoteImageProxyURL,
|
||||
RemoteImageProxyOptions: &test.RemoteImageProxyOptions,
|
||||
Enable: &test.Enable,
|
||||
ImageProxyType: &test.ImageProxyType,
|
||||
}
|
||||
|
||||
appErr := ips.isValid()
|
||||
|
||||
@@ -1673,10 +1673,6 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
},
|
||||
help_text_markdown: false,
|
||||
options: [
|
||||
{
|
||||
value: 'atmos/camo',
|
||||
display_name: defineMessage({id: 'atmos/camo', defaultMessage: 'atmos/camo'}),
|
||||
},
|
||||
{
|
||||
value: 'local',
|
||||
display_name: defineMessage({id: 'local', defaultMessage: 'local'}),
|
||||
@@ -1687,28 +1683,6 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
it.stateIsFalse('ImageProxySettings.Enable'),
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'ImageProxySettings.RemoteImageProxyURL',
|
||||
label: defineMessage({id: 'admin.image.proxyURL', defaultMessage: 'Remote Image Proxy URL:'}),
|
||||
help_text: defineMessage({id: 'admin.image.proxyURLDescription', defaultMessage: 'URL of your remote image proxy server.'}),
|
||||
isDisabled: it.any(
|
||||
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.IMAGE_PROXY)),
|
||||
it.stateIsFalse('ImageProxySettings.Enable'),
|
||||
it.stateEquals('ImageProxySettings.ImageProxyType', 'local'),
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'ImageProxySettings.RemoteImageProxyOptions',
|
||||
label: defineMessage({id: 'admin.image.proxyOptions', defaultMessage: 'Remote Image Proxy Options:'}),
|
||||
help_text: defineMessage({id: 'admin.image.proxyOptionsDescription', defaultMessage: 'Additional options such as the URL signing key. Refer to your image proxy documentation to learn more about what options are supported.'}),
|
||||
isDisabled: it.any(
|
||||
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.IMAGE_PROXY)),
|
||||
it.stateIsFalse('ImageProxySettings.Enable'),
|
||||
it.stateEquals('ImageProxySettings.ImageProxyType', 'local'),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1689,12 +1689,8 @@
|
||||
"admin.image.maxFileSizeDescription": "Maximum file size for message attachments in megabytes. Caution: Verify server memory can support your setting choice. Large file sizes increase the risk of server crashes and failed uploads due to network interruptions.",
|
||||
"admin.image.maxFileSizeExample": "50",
|
||||
"admin.image.maxFileSizeTitle": "Maximum File Size:",
|
||||
"admin.image.proxyOptions": "Remote Image Proxy Options:",
|
||||
"admin.image.proxyOptionsDescription": "Additional options such as the URL signing key. Refer to your image proxy documentation to learn more about what options are supported.",
|
||||
"admin.image.proxyType": "Image Proxy Type:",
|
||||
"admin.image.proxyTypeDescription": "Configure an image proxy to load all Markdown images through a proxy. The image proxy prevents users from making insecure image requests, provides caching for increased performance, and automates image adjustments such as resizing. See <link>documentation</link> to learn more.",
|
||||
"admin.image.proxyURL": "Remote Image Proxy URL:",
|
||||
"admin.image.proxyURLDescription": "URL of your remote image proxy server.",
|
||||
"admin.image.publicLinkDescription": "32-character salt added to signing of public links. Randomly generated on install. Select \"Regenerate\" to create new salt.",
|
||||
"admin.image.publicLinkTitle": "Public Link Salt:",
|
||||
"admin.image.shareDescription": "Allow users to share public links to files and images.",
|
||||
@@ -4191,7 +4187,6 @@
|
||||
"apps.suggestion.no_dynamic": "No data was returned for dynamic suggestions",
|
||||
"apps.suggestion.no_static": "No matching options.",
|
||||
"apps.suggestion.no_suggestion": "No matching suggestions.",
|
||||
"atmos/camo": "atmos/camo",
|
||||
"attachment.footerIconAltText": "Attachment footer icon",
|
||||
"attachment.imageAltText": "Attachment image",
|
||||
"audit_table.accountActive": "Account activated",
|
||||
|
||||
@@ -1027,8 +1027,6 @@ export type GuestAccountsSettings = {
|
||||
export type ImageProxySettings = {
|
||||
Enable: boolean;
|
||||
ImageProxyType: string;
|
||||
RemoteImageProxyURL: string;
|
||||
RemoteImageProxyOptions: string;
|
||||
};
|
||||
|
||||
export type CloudSettings = {
|
||||
|
||||
Reference in New Issue
Block a user