feat: add configurable transport-independent CORS policy
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/contract"
|
||||
"git.yuntuo.hk/k2safe/go-omni/cors"
|
||||
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
|
||||
@@ -28,6 +29,7 @@ type TLSConfig struct {
|
||||
|
||||
// Config controls the default runtime behavior of an App.
|
||||
type Config struct {
|
||||
CORS cors.Config `json:"cors"`
|
||||
Request contract.RequestPolicy `json:"request"`
|
||||
Address string `json:"address"`
|
||||
Transport string `json:"transport"`
|
||||
@@ -61,6 +63,9 @@ func Default() Config {
|
||||
|
||||
// Validate checks configuration values before an application starts.
|
||||
func (c Config) Validate() error {
|
||||
if err := c.CORS.Validate(); err != nil {
|
||||
return AtField("cors", err)
|
||||
}
|
||||
if err := c.Lifecycle.Validate(); err != nil {
|
||||
return AtField("lifecycle", err)
|
||||
}
|
||||
@@ -188,12 +193,14 @@ func WithTrustedProxies(proxies ...string) Option {
|
||||
// WithConfig applies a complete configuration loaded from a file or
|
||||
// environment. Later options can still override individual fields.
|
||||
func WithConfig(settings Config) Option {
|
||||
settings.CORS = settings.CORS.Clone()
|
||||
settings.Response = settings.Response.Clone()
|
||||
settings.Errors = settings.Errors.Clone()
|
||||
settings.TrustedProxies = append([]string(nil), settings.TrustedProxies...)
|
||||
settings.Log.RedactKeys = append([]string(nil), settings.Log.RedactKeys...)
|
||||
return func(cfg *Config) {
|
||||
*cfg = settings
|
||||
cfg.CORS = settings.CORS.Clone()
|
||||
cfg.Response = settings.Response.Clone()
|
||||
cfg.Errors = settings.Errors.Clone()
|
||||
cfg.TrustedProxies = append([]string(nil), settings.TrustedProxies...)
|
||||
@@ -201,6 +208,12 @@ func WithConfig(settings Config) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithCORS installs an app-local CORS policy, before routing and authentication.
|
||||
func WithCORS(settings cors.Config) Option {
|
||||
settings = settings.Clone()
|
||||
return func(cfg *Config) { cfg.CORS = settings.Clone() }
|
||||
}
|
||||
|
||||
// WithResponse configures this application's default JSON envelope.
|
||||
func WithResponse(settings response.Config) Option {
|
||||
settings = settings.Clone()
|
||||
|
||||
51
config/cors_test.go
Normal file
51
config/cors_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/config"
|
||||
)
|
||||
|
||||
type corsProject struct {
|
||||
Server config.Config `json:"server"`
|
||||
}
|
||||
|
||||
func (c corsProject) Validate() error { return config.AtField("server", c.Server.Validate()) }
|
||||
|
||||
func TestCORSConfiguration(t *testing.T) {
|
||||
t.Setenv("WEB_ORIGIN", "https://web.example")
|
||||
for ext, source := range map[string]string{
|
||||
"yaml": "server:\n cors:\n enabled: true\n allowedOrigins:\n - '${WEB_ORIGIN}'\n allowedMethods: [GET, POST]\n allowedHeaders: [Authorization, Content-Type]\n maxAge: 10m\n",
|
||||
"json": `{"server":{"cors":{"enabled":true,"allowedOrigins":["${WEB_ORIGIN}"],"allowedMethods":["GET","POST"],"allowedHeaders":["Authorization","Content-Type"],"maxAge":"10m"}}}`,
|
||||
"xml": `<config><server><cors><enabled type="bool">true</enabled><allowedOrigins type="array"><item>${WEB_ORIGIN}</item></allowedOrigins><allowedMethods type="array"><item>GET</item><item>POST</item></allowedMethods><allowedHeaders type="array"><item>Authorization</item><item>Content-Type</item></allowedHeaders><maxAge>10m</maxAge></cors></server></config>`,
|
||||
} {
|
||||
t.Run(ext, func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config."+ext)
|
||||
if err := os.WriteFile(path, []byte(source), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := config.LoadProject(path, corsProject{Server: config.Default()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !cfg.Server.CORS.Enabled || cfg.Server.CORS.AllowedOrigins[0] != "https://web.example" || cfg.Server.CORS.MaxAge != 10*time.Minute {
|
||||
t.Fatal(cfg.Server.CORS)
|
||||
}
|
||||
})
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "bad.yaml")
|
||||
if err := os.WriteFile(path, []byte("server:\n cors:\n enabled: true\n allowedOrigins:\n - '${WEB_ORIGIN}'\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("WEB_ORIGIN", "https://user:secret@web.example")
|
||||
_, err := config.LoadProject(path, corsProject{Server: config.Default()})
|
||||
var d *config.Error
|
||||
if !errors.As(err, &d) || d.Field != "server.cors.allowedOrigins[0]" || d.Env != "WEB_ORIGIN" || d.Line != 5 || strings.Contains(err.Error(), "secret") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user