Config: Add options to configure CORS origin, headers and methods #3931
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
4e7a61ffe5
commit
239708f00f
13 changed files with 122 additions and 61 deletions
|
@ -464,6 +464,21 @@ func (c *Config) CdnVideo() bool {
|
|||
return c.options.CdnVideo
|
||||
}
|
||||
|
||||
// CORSOrigin returns the value for the Access-Control-Allow-Origin header, if any.
|
||||
func (c *Config) CORSOrigin() string {
|
||||
return clean.Header(c.options.CORSOrigin)
|
||||
}
|
||||
|
||||
// CORSHeaders returns the value for the Access-Control-Allow-Headers header, if any.
|
||||
func (c *Config) CORSHeaders() string {
|
||||
return clean.Header(c.options.CORSHeaders)
|
||||
}
|
||||
|
||||
// CORSMethods returns the value for the Access-Control-Allow-Methods header, if any.
|
||||
func (c *Config) CORSMethods() string {
|
||||
return clean.Header(c.options.CORSMethods)
|
||||
}
|
||||
|
||||
// ContentUri returns the content delivery URI.
|
||||
func (c *Config) ContentUri() string {
|
||||
return c.CdnUrl(c.ApiUri())
|
||||
|
|
|
@ -83,12 +83,6 @@ func (c *Config) HttpCompression() string {
|
|||
return strings.ToLower(strings.TrimSpace(c.options.HttpCompression))
|
||||
}
|
||||
|
||||
// HttpCORS checks of Cross-Origin Resource Sharing (CORS) should be allowed,
|
||||
// so the "Access-Control-Allow-Origin" response header should be set to "*".
|
||||
func (c *Config) HttpCORS() bool {
|
||||
return c.options.HttpCORS
|
||||
}
|
||||
|
||||
// HttpCachePublic checks whether static content may be cached by a CDN or caching proxy.
|
||||
func (c *Config) HttpCachePublic() bool {
|
||||
if c.options.HttpCachePublic {
|
||||
|
|
|
@ -62,22 +62,6 @@ func TestConfig_HttpCompression(t *testing.T) {
|
|||
assert.Equal(t, "", c.HttpCompression())
|
||||
}
|
||||
|
||||
func TestConfig_HttpCORS(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
c.Options().CdnUrl = ""
|
||||
c.Options().HttpCORS = false
|
||||
assert.False(t, c.HttpCORS())
|
||||
c.Options().CdnUrl = "https://cdn.com/"
|
||||
assert.False(t, c.HttpCORS())
|
||||
c.Options().CdnUrl = ""
|
||||
assert.False(t, c.HttpCORS())
|
||||
c.Options().HttpCORS = true
|
||||
assert.True(t, c.HttpCORS())
|
||||
c.Options().HttpCORS = false
|
||||
assert.False(t, c.HttpCORS())
|
||||
}
|
||||
|
||||
func TestConfig_HttpCachePublic(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
@ -471,6 +472,39 @@ func TestConfig_CdnVideo(t *testing.T) {
|
|||
assert.False(t, c.CdnVideo())
|
||||
}
|
||||
|
||||
func TestConfig_CORSOrigin(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
c.Options().CORSOrigin = ""
|
||||
assert.Equal(t, "", c.CORSOrigin())
|
||||
c.Options().CORSOrigin = "*"
|
||||
assert.Equal(t, "*", c.CORSOrigin())
|
||||
c.Options().CORSOrigin = "https://developer.mozilla.org"
|
||||
assert.Equal(t, "https://developer.mozilla.org", c.CORSOrigin())
|
||||
c.Options().CORSOrigin = ""
|
||||
assert.Equal(t, "", c.CORSOrigin())
|
||||
}
|
||||
|
||||
func TestConfig_CORSHeaders(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "", c.CORSHeaders())
|
||||
c.Options().CORSHeaders = header.DefaultAccessControlAllowHeaders
|
||||
assert.Equal(t, header.DefaultAccessControlAllowHeaders, c.CORSHeaders())
|
||||
c.Options().CORSHeaders = ""
|
||||
assert.Equal(t, "", c.CORSHeaders())
|
||||
}
|
||||
|
||||
func TestConfig_CORSMethods(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "", c.CORSMethods())
|
||||
c.Options().CORSMethods = header.DefaultAccessControlAllowMethods
|
||||
assert.Equal(t, header.DefaultAccessControlAllowMethods, c.CORSMethods())
|
||||
c.Options().CORSMethods = ""
|
||||
assert.Equal(t, "", c.CORSMethods())
|
||||
}
|
||||
|
||||
func TestConfig_ContentUri(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
|
|
|
@ -384,16 +384,6 @@ var Flags = CliFlags{
|
|||
Value: "",
|
||||
EnvVar: EnvVar("WALLPAPER_URI"),
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "cdn-url",
|
||||
Usage: "content delivery network `URL`",
|
||||
EnvVar: EnvVar("CDN_URL"),
|
||||
}}, {
|
||||
Flag: cli.BoolFlag{
|
||||
Name: "cdn-video",
|
||||
Usage: "stream videos over the specified CDN",
|
||||
EnvVar: EnvVar("CDN_VIDEO"),
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "site-url, url",
|
||||
Usage: "public site `URL`",
|
||||
|
@ -427,6 +417,34 @@ var Flags = CliFlags{
|
|||
Usage: "sharing preview image `URL`",
|
||||
EnvVar: EnvVar("SITE_PREVIEW"),
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "cdn-url",
|
||||
Usage: "content delivery network `URL`",
|
||||
EnvVar: EnvVar("CDN_URL"),
|
||||
}}, {
|
||||
Flag: cli.BoolFlag{
|
||||
Name: "cdn-video",
|
||||
Usage: "stream videos over the specified CDN",
|
||||
EnvVar: EnvVar("CDN_VIDEO"),
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "cors-origin",
|
||||
Usage: "origin `URL` from which browsers are allowed to perform cross-origin requests (leave empty to disable or use * to allow all)",
|
||||
EnvVar: EnvVar("CORS_ORIGIN"),
|
||||
Value: header.DefaultAccessControlAllowOrigin,
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "cors-headers",
|
||||
Usage: "one or more `HEADERS` that browsers should see when performing a cross-origin request",
|
||||
EnvVar: EnvVar("CORS_HEADERS"),
|
||||
Value: header.DefaultAccessControlAllowHeaders,
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "cors-methods",
|
||||
Usage: "one or more `METHODS` that may be used when performing a cross-origin request",
|
||||
EnvVar: EnvVar("CORS_METHODS"),
|
||||
Value: header.DefaultAccessControlAllowMethods,
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "https-proxy",
|
||||
Usage: "proxy server `URL` to be used for outgoing connections *optional*",
|
||||
|
@ -491,11 +509,6 @@ var Flags = CliFlags{
|
|||
Usage: "Web server compression `METHOD` (gzip, none)",
|
||||
EnvVar: EnvVar("HTTP_COMPRESSION"),
|
||||
}}, {
|
||||
Flag: cli.BoolFlag{
|
||||
Name: "http-cors",
|
||||
Usage: "allow Cross-Origin Resource Sharing (CORS)",
|
||||
EnvVar: EnvVar("HTTP_CORS"),
|
||||
}}, {
|
||||
Flag: cli.BoolFlag{
|
||||
Name: "http-cache-public",
|
||||
Usage: "allow static content to be cached by a CDN or caching proxy",
|
||||
|
|
|
@ -93,14 +93,17 @@ type Options struct {
|
|||
LegalInfo string `yaml:"LegalInfo" json:"LegalInfo" flag:"legal-info"`
|
||||
LegalUrl string `yaml:"LegalUrl" json:"LegalUrl" flag:"legal-url"`
|
||||
WallpaperUri string `yaml:"WallpaperUri" json:"WallpaperUri" flag:"wallpaper-uri"`
|
||||
CdnUrl string `yaml:"CdnUrl" json:"CdnUrl" flag:"cdn-url"`
|
||||
CdnVideo bool `yaml:"CdnVideo" json:"CdnVideo" flag:"cdn-video"`
|
||||
SiteUrl string `yaml:"SiteUrl" json:"SiteUrl" flag:"site-url"`
|
||||
SiteAuthor string `yaml:"SiteAuthor" json:"SiteAuthor" flag:"site-author"`
|
||||
SiteTitle string `yaml:"SiteTitle" json:"SiteTitle" flag:"site-title"`
|
||||
SiteCaption string `yaml:"SiteCaption" json:"SiteCaption" flag:"site-caption"`
|
||||
SiteDescription string `yaml:"SiteDescription" json:"SiteDescription" flag:"site-description"`
|
||||
SitePreview string `yaml:"SitePreview" json:"SitePreview" flag:"site-preview"`
|
||||
CdnUrl string `yaml:"CdnUrl" json:"CdnUrl" flag:"cdn-url"`
|
||||
CdnVideo bool `yaml:"CdnVideo" json:"CdnVideo" flag:"cdn-video"`
|
||||
CORSOrigin string `yaml:"CORSOrigin" json:"-" flag:"cors-origin"`
|
||||
CORSHeaders string `yaml:"CORSHeaders" json:"-" flag:"cors-headers"`
|
||||
CORSMethods string `yaml:"CORSMethods" json:"-" flag:"cors-methods"`
|
||||
HttpsProxy string `yaml:"HttpsProxy" json:"HttpsProxy" flag:"https-proxy"`
|
||||
HttpsProxyInsecure bool `yaml:"HttpsProxyInsecure" json:"HttpsProxyInsecure" flag:"https-proxy-insecure"`
|
||||
TrustedProxies []string `yaml:"TrustedProxies" json:"-" flag:"trusted-proxy"`
|
||||
|
@ -113,7 +116,6 @@ type Options struct {
|
|||
TLSKey string `yaml:"TLSKey" json:"TLSKey" flag:"tls-key"`
|
||||
HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"`
|
||||
HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"`
|
||||
HttpCORS bool `yaml:"HttpCORS" json:"-" flag:"http-cors"`
|
||||
HttpCachePublic bool `yaml:"HttpCachePublic" json:"HttpCachePublic" flag:"http-cache-public"`
|
||||
HttpCacheMaxAge int `yaml:"HttpCacheMaxAge" json:"HttpCacheMaxAge" flag:"http-cache-maxage"`
|
||||
HttpVideoMaxAge int `yaml:"HttpVideoMaxAge" json:"HttpVideoMaxAge" flag:"http-video-maxage"`
|
||||
|
|
|
@ -127,8 +127,6 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||
{"wallpaper-uri", c.WallpaperUri()},
|
||||
|
||||
// Site Infos.
|
||||
{"cdn-url", c.CdnUrl("/")},
|
||||
{"cdn-video", fmt.Sprintf("%t", c.CdnVideo())},
|
||||
{"site-url", c.SiteUrl()},
|
||||
{"site-https", fmt.Sprintf("%t", c.SiteHttps())},
|
||||
{"site-domain", c.SiteDomain()},
|
||||
|
@ -138,6 +136,13 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||
{"site-description", c.SiteDescription()},
|
||||
{"site-preview", c.SitePreview()},
|
||||
|
||||
// CDN and Cross-Origin Resource Sharing (CORS).
|
||||
{"cdn-url", c.CdnUrl("/")},
|
||||
{"cdn-video", fmt.Sprintf("%t", c.CdnVideo())},
|
||||
{"cors-origin", c.CORSOrigin()},
|
||||
{"cors-headers", c.CORSHeaders()},
|
||||
{"cors-methods", c.CORSMethods()},
|
||||
|
||||
// URIs.
|
||||
{"base-uri", c.BaseUri("/")},
|
||||
{"api-uri", c.ApiUri()},
|
||||
|
@ -160,7 +165,6 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||
{"tls-key", c.TLSKey()},
|
||||
{"http-mode", c.HttpMode()},
|
||||
{"http-compression", c.HttpCompression()},
|
||||
{"http-cors", fmt.Sprintf("%t", c.HttpCORS())},
|
||||
{"http-cache-public", fmt.Sprintf("%t", c.HttpCachePublic())},
|
||||
{"http-cache-maxage", fmt.Sprintf("%d", c.HttpCacheMaxAge())},
|
||||
{"http-video-maxage", fmt.Sprintf("%d", c.HttpVideoMaxAge())},
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
)
|
||||
|
||||
// Security adds common HTTP security headers to the response.
|
||||
var Security = func(conf *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Allow Cross-Origin Resource Sharing (CORS)?
|
||||
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin#cors_and_caching
|
||||
if conf.HttpCORS() {
|
||||
c.Header(header.AccessControlAllowOrigin, header.Any)
|
||||
} else if origin := c.GetHeader(header.Origin); origin != "" {
|
||||
// Automatically set the "Access-Control-Allow-Origin" response header
|
||||
// based on the "Origin" value of the request.
|
||||
c.Header(header.AccessControlAllowOrigin, clean.Header(origin))
|
||||
c.Header(header.Vary, header.Origin)
|
||||
// If permitted, set CORS headers (Cross-Origin Resource Sharing).
|
||||
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
|
||||
if origin := conf.CORSOrigin(); origin != "" {
|
||||
c.Header(header.AccessControlAllowOrigin, origin)
|
||||
|
||||
// Add additional information to preflight OPTION requests.
|
||||
if c.Request.Method == http.MethodOptions {
|
||||
c.Header(header.AccessControlAllowHeaders, conf.CORSHeaders())
|
||||
c.Header(header.AccessControlAllowMethods, conf.CORSMethods())
|
||||
c.Header(header.AccessControlMaxAge, header.DefaultAccessControlMaxAge)
|
||||
}
|
||||
}
|
||||
|
||||
// Set Content Security Policy.
|
||||
|
|
|
@ -9,7 +9,7 @@ func Header(s string) string {
|
|||
result := make([]rune, 0, len(s))
|
||||
|
||||
for _, r := range s {
|
||||
if r > 32 && r < 127 {
|
||||
if r > 31 && r < 127 {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ func TestHeader(t *testing.T) {
|
|||
})
|
||||
t.Run("Emoji", func(t *testing.T) {
|
||||
result := Header("Hello 👍")
|
||||
assert.Equal(t, "Hello", result)
|
||||
assert.Equal(t, "Hello ", result)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
package header
|
||||
|
||||
// Cross-Origin Resource Sharing (CORS) headers.
|
||||
const (
|
||||
AccessControlAllowOrigin = "Access-Control-Allow-Origin" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
|
||||
AccessControlAllowCredentials = "Access-Control-Allow-Credentials" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
|
||||
AccessControlAllowHeaders = "Access-Control-Allow-Headers" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
|
||||
AccessControlAllowMethods = "Access-Control-Allow-Methods" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
|
||||
AccessControlMaxAge = "Access-Control-Max-Age" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
|
||||
)
|
||||
|
||||
// CORS header defaults.
|
||||
var (
|
||||
DefaultAccessControlAllowOrigin = ""
|
||||
DefaultAccessControlAllowCredentials = ""
|
||||
DefaultAccessControlAllowHeaders = "Origin, Accept, Accept-Ranges, Content-Range"
|
||||
DefaultAccessControlAllowMethods = "GET, HEAD, OPTIONS"
|
||||
DefaultAccessControlMaxAge = "3600"
|
||||
)
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
package header
|
||||
|
||||
var (
|
||||
DefaultContentSecurityPolicy = "frame-ancestors 'none';"
|
||||
DefaultFrameOptions = Deny
|
||||
)
|
|
@ -1,5 +1,6 @@
|
|||
package header
|
||||
|
||||
// HTTP/HTTPS security headers.
|
||||
const (
|
||||
StrictTransportSecurity = "Strict-Transport-Security" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
|
||||
ContentSecurityPolicy = "Content-Security-Policy" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
|
||||
|
@ -10,3 +11,9 @@ const (
|
|||
FrameOptions = "X-Frame-Options" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
|
||||
ForwardedProto = "X-Forwarded-Proto" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
|
||||
)
|
||||
|
||||
// Security header defaults.
|
||||
var (
|
||||
DefaultContentSecurityPolicy = "frame-ancestors 'none';"
|
||||
DefaultFrameOptions = Deny
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue