From b9cd12ea631092a9f740c1daf9b8ac5a1fd7b361 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 9 Nov 2023 10:14:48 +1100 Subject: [PATCH] fix: return CORS headers in ingress responses (#563) Co-authored-by: github-actions[bot] --- Dockerfile.controller | 2 +- backend/common/cors/cors.go | 12 ++++++++++++ backend/common/rpc/server.go | 18 +----------------- backend/controller/controller.go | 13 ++++++++++--- cmd/ftl/cmd_serve.go | 16 +++++++++------- examples/go.mod | 1 - examples/go.sum | 2 -- examples/online-boutique/go.mod | 1 - examples/online-boutique/go.sum | 2 -- examples/online-boutique/services/ad/ad.go | 3 ++- frontend/cors.go | 11 ----------- frontend/local.go | 18 ++++++------------ frontend/release.go | 14 ++++++++++---- 13 files changed, 51 insertions(+), 62 deletions(-) create mode 100644 backend/common/cors/cors.go delete mode 100644 frontend/cors.go diff --git a/Dockerfile.controller b/Dockerfile.controller index 2402e77ebc..0ae2100c10 100644 --- a/Dockerfile.controller +++ b/Dockerfile.controller @@ -40,7 +40,7 @@ EXPOSE 8892 ENV FTL_CONTROLLER_BIND="http://0.0.0.0:8892" ENV FTL_CONTROLLER_ADVERTISE="http://127.0.0.1:8892" -ENV FTL_CONTROLLER_ALLOW_ORIGIN="*" +ENV FTL_CONTROLLER_CONSOLE_URL="*" ENV FTL_CONTROLLER_DSN="postgres://host.docker.internal/ftl?sslmode=disable&user=postgres&password=secret" CMD ["/root/ftl-controller"] diff --git a/backend/common/cors/cors.go b/backend/common/cors/cors.go new file mode 100644 index 0000000000..7bb0672f88 --- /dev/null +++ b/backend/common/cors/cors.go @@ -0,0 +1,12 @@ +package cors + +import ( + "net/http" + + "github.com/rs/cors" +) + +func Middleware(allowOrigins []string, next http.Handler) http.Handler { + c := cors.New(cors.Options{AllowedOrigins: allowOrigins}) + return c.Handler(next) +} diff --git a/backend/common/rpc/server.go b/backend/common/rpc/server.go index 339643a206..0bb352da54 100644 --- a/backend/common/rpc/server.go +++ b/backend/common/rpc/server.go @@ -12,7 +12,6 @@ import ( "connectrpc.com/grpcreflect" "github.com/alecthomas/concurrency" "github.com/alecthomas/errors" - "github.com/rs/cors" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" @@ -82,22 +81,7 @@ func NewServer(ctx context.Context, listen *url.URL, options ...Option) (*Server reflector := grpcreflect.NewStaticReflector(opts.reflectionPaths...) opts.mux.Handle(grpcreflect.NewHandlerV1(reflector)) opts.mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector)) - - // TODO: Is this a good idea? Who knows! - crs := cors.New(cors.Options{ - AllowedOrigins: []string{listen.String(), "http://ftl.localtest.me"}, - AllowedMethods: []string{ - http.MethodHead, - http.MethodGet, - http.MethodPost, - http.MethodPut, - http.MethodPatch, - http.MethodDelete, - }, - AllowedHeaders: []string{"*"}, - AllowCredentials: false, - }) - root := crs.Handler(ContextValuesMiddleware(ctx, opts.mux)) + root := ContextValuesMiddleware(ctx, opts.mux) http1Server := &http.Server{ Handler: h2c.NewHandler(root, &http2.Server{}), diff --git a/backend/controller/controller.go b/backend/controller/controller.go index 65ccb12041..c01381994b 100644 --- a/backend/controller/controller.go +++ b/backend/controller/controller.go @@ -29,6 +29,7 @@ import ( "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/TBD54566975/ftl/backend/common/cors" "github.com/TBD54566975/ftl/backend/common/log" "github.com/TBD54566975/ftl/backend/common/model" "github.com/TBD54566975/ftl/backend/common/rpc" @@ -48,7 +49,8 @@ import ( type Config struct { Bind *url.URL `help:"Socket to bind to." default:"http://localhost:8892" env:"FTL_CONTROLLER_BIND"` Advertise *url.URL `help:"Endpoint the Controller should advertise (must be unique across the cluster, defaults to --bind if omitted)." env:"FTL_CONTROLLER_ADVERTISE"` - AllowOrigin string `help:"Allow CORS requests from this origin." default:"*" env:"FTL_CONTROLLER_ALLOW_ORIGIN"` + ConsoleURL *url.URL `help:"The public URL of the console (for CORS)." env:"FTL_CONTROLLER_CONSOLE_URL"` + AllowOrigins []*url.URL `help:"Allow CORS requests to ingress endpoints from these origins." env:"FTL_CONTROLLER_ALLOW_ORIGIN"` ContentTime time.Time `help:"Time to use for console resource timestamps." default:"${timestamp=1970-01-01T00:00:00Z}"` Key model.ControllerKey `help:"Controller key (auto)." placeholder:"C" default:"C00000000000000000000000000"` DSN string `help:"DAL DSN." default:"postgres://localhost/ftl?sslmode=disable&user=postgres&password=secret" env:"FTL_CONTROLLER_DSN"` @@ -73,7 +75,7 @@ func Start(ctx context.Context, config Config, runnerScaling scaling.RunnerScali logger := log.FromContext(ctx) logger.Infof("Starting FTL controller") - c, err := frontend.Server(ctx, config.ContentTime, config.AllowOrigin) + c, err := frontend.Server(ctx, config.ContentTime, config.ConsoleURL) if err != nil { return errors.WithStack(err) } @@ -96,11 +98,16 @@ func Start(ctx context.Context, config Config, runnerScaling scaling.RunnerScali console := NewConsoleService(dal) + ingressHandler := http.StripPrefix("/ingress", svc) + if len(config.AllowOrigins) > 0 { + ingressHandler = cors.Middleware(slices.Map(config.AllowOrigins, func(u *url.URL) string { return u.String() }), ingressHandler) + } + return rpc.Serve(ctx, config.Bind, rpc.GRPC(ftlv1connect.NewVerbServiceHandler, svc), rpc.GRPC(ftlv1connect.NewControllerServiceHandler, svc), rpc.GRPC(pbconsoleconnect.NewConsoleServiceHandler, console), - rpc.HTTP("/ingress/", http.StripPrefix("/ingress", svc)), + rpc.HTTP("/ingress/", ingressHandler), rpc.HTTP("/", c), ) } diff --git a/cmd/ftl/cmd_serve.go b/cmd/ftl/cmd_serve.go index 01a402b77c..8b35c07527 100644 --- a/cmd/ftl/cmd_serve.go +++ b/cmd/ftl/cmd_serve.go @@ -20,11 +20,12 @@ import ( ) type serveCmd struct { - Bind *url.URL `help:"Starting endpoint to bind to and advertise to. Each controller and runner will increment the port by 1" default:"http://localhost:8892"` - DBPort int `help:"Port to use for the database." default:"5433"` - Recreate bool `help:"Recreate the database even if it already exists." default:"false"` - Controllers int `short:"c" help:"Number of controllers to start." default:"1"` - Runners int `short:"r" help:"Number of runners to start." default:"0"` + Bind *url.URL `help:"Starting endpoint to bind to and advertise to. Each controller and runner will increment the port by 1" default:"http://localhost:8892"` + AllowOrigins []*url.URL `help:"Allow CORS requests to ingress endpoints from these origins." env:"FTL_CONTROLLER_ALLOW_ORIGIN"` + DBPort int `help:"Port to use for the database." default:"5433"` + Recreate bool `help:"Recreate the database even if it already exists." default:"false"` + Controllers int `short:"c" help:"Number of controllers to start." default:"1"` + Runners int `short:"r" help:"Number of runners to start." default:"0"` } const ftlContainerName = "ftl-db" @@ -59,8 +60,9 @@ func (s *serveCmd) Run(ctx context.Context) error { for i := 0; i < s.Controllers; i++ { i := i config := controller.Config{ - Bind: controllerAddresses[i], - DSN: dsn, + Bind: controllerAddresses[i], + DSN: dsn, + AllowOrigins: s.AllowOrigins, } if err := kong.ApplyDefaults(&config); err != nil { return errors.WithStack(err) diff --git a/examples/go.mod b/examples/go.mod index b0a26358b8..59c4254f83 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -25,7 +25,6 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/oklog/ulid/v2 v2.1.0 // indirect - github.com/rs/cors v1.9.0 // indirect github.com/swaggest/jsonschema-go v0.3.62 // indirect github.com/swaggest/refl v1.3.0 // indirect github.com/zalando/go-keyring v0.2.1 // indirect diff --git a/examples/go.sum b/examples/go.sum index e157cfe391..563ce2137d 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -62,8 +62,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= -github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/examples/online-boutique/go.mod b/examples/online-boutique/go.mod index 6f115d2f61..14f10ca944 100644 --- a/examples/online-boutique/go.mod +++ b/examples/online-boutique/go.mod @@ -29,7 +29,6 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/oklog/ulid/v2 v2.1.0 // indirect - github.com/rs/cors v1.9.0 // indirect github.com/swaggest/jsonschema-go v0.3.62 // indirect github.com/swaggest/refl v1.3.0 // indirect github.com/zalando/go-keyring v0.2.1 // indirect diff --git a/examples/online-boutique/go.sum b/examples/online-boutique/go.sum index 0321db7610..5f63250c34 100644 --- a/examples/online-boutique/go.sum +++ b/examples/online-boutique/go.sum @@ -64,8 +64,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= -github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/examples/online-boutique/services/ad/ad.go b/examples/online-boutique/services/ad/ad.go index a9ca57526e..78af273e05 100644 --- a/examples/online-boutique/services/ad/ad.go +++ b/examples/online-boutique/services/ad/ad.go @@ -29,7 +29,8 @@ type Ad struct { } type AdResponse struct { - Ads []Ad + Name string + Ads []Ad } //ftl:verb diff --git a/frontend/cors.go b/frontend/cors.go deleted file mode 100644 index 9a76f9ead2..0000000000 --- a/frontend/cors.go +++ /dev/null @@ -1,11 +0,0 @@ -package frontend - -import ( - "net/http" -) - -func writeCORSHeaders(w http.ResponseWriter, allowOrigin string) { - w.Header().Set("Access-Control-Allow-Origin", allowOrigin) - w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") - w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") -} diff --git a/frontend/local.go b/frontend/local.go index 16cb220338..948fc01c59 100644 --- a/frontend/local.go +++ b/frontend/local.go @@ -11,6 +11,7 @@ import ( "github.com/alecthomas/errors" + "github.com/TBD54566975/ftl/backend/common/cors" "github.com/TBD54566975/ftl/backend/common/exec" "github.com/TBD54566975/ftl/backend/common/log" ) @@ -18,7 +19,7 @@ import ( var consoleURL, _ = url.Parse("http://localhost:5173") var proxy = httputil.NewSingleHostReverseProxy(consoleURL) -func Server(ctx context.Context, timestamp time.Time, allowOrigin string) (http.Handler, error) { +func Server(ctx context.Context, timestamp time.Time, allowOrigin *url.URL) (http.Handler, error) { logger := log.FromContext(ctx) logger.Infof("Building console...") @@ -33,16 +34,9 @@ func Server(ctx context.Context, timestamp time.Time, allowOrigin string) (http. } logger.Infof("Console started") - return http.HandlerFunc(handler(allowOrigin)), nil -} - -func handler(allowOrigin string) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - writeCORSHeaders(w, allowOrigin) - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) - return - } - proxy.ServeHTTP(w, r) + if allowOrigin == nil { + return proxy, nil } + + return cors.Middleware([]string{allowOrigin.String()}, proxy), nil } diff --git a/frontend/release.go b/frontend/release.go index 1ead3eebb1..014954490d 100644 --- a/frontend/release.go +++ b/frontend/release.go @@ -8,24 +8,26 @@ import ( "io" "io/fs" "net/http" + "net/url" "os" "path" "strings" "time" "github.com/alecthomas/errors" + + "github.com/TBD54566975/ftl/backend/common/cors" ) //go:embed all:dist var build embed.FS -func Server(ctx context.Context, timestamp time.Time, allowOrigin string) (http.Handler, error) { +func Server(ctx context.Context, timestamp time.Time, allowOrigin *url.URL) (http.Handler, error) { dir, err := fs.Sub(build, "dist") if err != nil { return nil, errors.WithStack(err) } - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - writeCORSHeaders(w, allowOrigin) + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var f fs.File var err error filePath := strings.TrimPrefix(r.URL.Path, "/") @@ -43,5 +45,9 @@ func Server(ctx context.Context, timestamp time.Time, allowOrigin string) (http. return } http.ServeContent(w, r, filePath, timestamp, f.(io.ReadSeeker)) - }), nil + }) + if allowOrigin != nil { + handler = cors.Middleware([]string{allowOrigin.String()}, handler) + } + return handler, nil }