Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
181 lines
5.4 KiB
Go
181 lines
5.4 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package imageproxy
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/configservice"
|
|
"github.com/mattermost/mattermost/server/public/shared/httpservice"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
)
|
|
|
|
var ErrNotEnabled = Error{errors.New("imageproxy.ImageProxy: image proxy not enabled")}
|
|
|
|
// An ImageProxy is the public interface for Mattermost's image proxy. An instance of ImageProxy should be created
|
|
// using MakeImageProxy which requires a configService and an HTTPService provided by the server.
|
|
type ImageProxy struct {
|
|
ConfigService configservice.ConfigService
|
|
configListenerID string
|
|
|
|
HTTPService httpservice.HTTPService
|
|
|
|
Logger *mlog.Logger
|
|
|
|
siteURL *url.URL
|
|
lock sync.RWMutex
|
|
backend ImageProxyBackend
|
|
}
|
|
|
|
// An ImageProxyBackend provides the functionality for different types of image proxies. An ImageProxy will construct
|
|
// the required backend depending on the ImageProxySettings provided by the ConfigService.
|
|
type ImageProxyBackend interface {
|
|
// GetImage provides a proxied image in response to an HTTP request.
|
|
GetImage(w http.ResponseWriter, r *http.Request, imageURL string)
|
|
|
|
// GetImageDirect returns a proxied image along with its content type.
|
|
GetImageDirect(imageURL string) (io.ReadCloser, string, error)
|
|
}
|
|
|
|
func MakeImageProxy(configService configservice.ConfigService, httpService httpservice.HTTPService, logger *mlog.Logger) *ImageProxy {
|
|
proxy := &ImageProxy{
|
|
ConfigService: configService,
|
|
HTTPService: httpService,
|
|
Logger: logger,
|
|
}
|
|
|
|
// 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.
|
|
siteURL, _ := url.Parse(*configService.Config().ServiceSettings.SiteURL)
|
|
proxy.siteURL = siteURL
|
|
|
|
proxy.configListenerID = proxy.ConfigService.AddConfigListener(proxy.OnConfigChange)
|
|
|
|
config := proxy.ConfigService.Config()
|
|
proxy.backend = proxy.makeBackend(config.ImageProxySettings)
|
|
|
|
return proxy
|
|
}
|
|
|
|
func (proxy *ImageProxy) makeBackend(proxySettings model.ImageProxySettings) ImageProxyBackend {
|
|
if !*proxySettings.Enable {
|
|
return nil
|
|
}
|
|
|
|
switch *proxySettings.ImageProxyType {
|
|
case model.ImageProxyTypeLocal:
|
|
return makeLocalBackend(proxy)
|
|
case model.ImageProxyTypeAtmosCamo:
|
|
return makeAtmosCamoBackend(proxy, proxySettings)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (proxy *ImageProxy) Close() {
|
|
proxy.lock.Lock()
|
|
defer proxy.lock.Unlock()
|
|
|
|
proxy.ConfigService.RemoveConfigListener(proxy.configListenerID)
|
|
}
|
|
|
|
func (proxy *ImageProxy) OnConfigChange(oldConfig, newConfig *model.Config) {
|
|
if *oldConfig.ServiceSettings.SiteURL != *newConfig.ServiceSettings.SiteURL ||
|
|
!reflect.DeepEqual(oldConfig.ImageProxySettings, newConfig.ImageProxySettings) {
|
|
proxy.lock.Lock()
|
|
defer proxy.lock.Unlock()
|
|
|
|
siteURL, _ := url.Parse(*newConfig.ServiceSettings.SiteURL)
|
|
proxy.siteURL = siteURL
|
|
|
|
proxy.backend = proxy.makeBackend(newConfig.ImageProxySettings)
|
|
}
|
|
}
|
|
|
|
// GetImage takes an HTTP request for an image and requests that image using the image proxy.
|
|
func (proxy *ImageProxy) GetImage(w http.ResponseWriter, r *http.Request, imageURL string) {
|
|
proxy.lock.RLock()
|
|
defer proxy.lock.RUnlock()
|
|
|
|
if proxy.backend == nil {
|
|
w.WriteHeader(http.StatusNotImplemented)
|
|
return
|
|
}
|
|
|
|
proxy.backend.GetImage(w, r, imageURL)
|
|
}
|
|
|
|
// GetImageDirect takes the URL of an image and returns the image along with its content type.
|
|
func (proxy *ImageProxy) GetImageDirect(imageURL string) (io.ReadCloser, string, error) {
|
|
proxy.lock.RLock()
|
|
defer proxy.lock.RUnlock()
|
|
|
|
if proxy.backend == nil {
|
|
return nil, "", ErrNotEnabled
|
|
}
|
|
|
|
return proxy.backend.GetImageDirect(imageURL)
|
|
}
|
|
|
|
// GetProxiedImageURL takes the URL of an image and returns a URL that can be used to view that image through the
|
|
// image proxy.
|
|
func (proxy *ImageProxy) GetProxiedImageURL(imageURL string) string {
|
|
if imageURL == "" || proxy.siteURL == nil || strings.HasPrefix(strings.ToLower(imageURL), "data:image/") {
|
|
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 proxy.siteURL.String()
|
|
}
|
|
// If host is same as siteURL host, return.
|
|
if parsedURL.Host == proxy.siteURL.Host {
|
|
return parsedURL.String()
|
|
}
|
|
|
|
// Handle protocol-relative URLs.
|
|
if parsedURL.Scheme == "" {
|
|
parsedURL.Scheme = proxy.siteURL.Scheme
|
|
}
|
|
|
|
// If it's a relative URL, fill up the hostname and return.
|
|
if parsedURL.Host == "" {
|
|
parsedURL.Host = proxy.siteURL.Host
|
|
return parsedURL.String()
|
|
}
|
|
|
|
return proxy.siteURL.String() + "/api/v4/image?url=" + url.QueryEscape(parsedURL.String())
|
|
}
|
|
|
|
// GetUnproxiedImageURL takes the URL of an image on the image proxy and returns the original URL of the image.
|
|
func (proxy *ImageProxy) GetUnproxiedImageURL(proxiedURL string) string {
|
|
return getUnproxiedImageURL(proxiedURL, *proxy.ConfigService.Config().ServiceSettings.SiteURL)
|
|
}
|
|
|
|
func getUnproxiedImageURL(proxiedURL, siteURL string) string {
|
|
if !strings.HasPrefix(proxiedURL, siteURL+"/api/v4/image?url=") {
|
|
return proxiedURL
|
|
}
|
|
|
|
parsed, err := url.Parse(proxiedURL)
|
|
if err != nil {
|
|
return proxiedURL
|
|
}
|
|
|
|
u := parsed.Query()["url"]
|
|
if len(u) == 0 {
|
|
return proxiedURL
|
|
}
|
|
|
|
return u[0]
|
|
}
|