From 3a7a03613418f1b07d38cc034a918855f894a4ea Mon Sep 17 00:00:00 2001 From: Ryan McGuire Date: Fri, 23 Jan 2026 20:20:32 -0500 Subject: [PATCH] add cors support --- go.mod | 17 +++++++++-------- go.sum | 34 ++++++++++++++++++---------------- pkg/config/config_test.go | 6 ++++++ pkg/config/types_http.go | 19 ++++++++++++++----- pkg/srv/http/http.go | 24 ++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index c35d1a6..d001c5a 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,9 @@ require ( buf.build/go/protovalidate v1.1.0 github.com/caarlos0/env/v11 v11.3.1 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.5 github.com/prometheus/client_golang v1.23.2 + github.com/rs/cors v1.11.1 github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.11.1 github.com/swaggest/jsonschema-go v0.3.79 @@ -45,7 +46,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect @@ -54,11 +55,11 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go.sum b/go.sum index 3d0c5a5..3cba7bb 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.5 h1:jP1RStw811EvUDzsUQ9oESqw2e4RqCjSAD9qIL8eMns= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.5/go.mod h1:WXNBZ64q3+ZUemCMXD9kYnr56H7CgZxDBHCVwstfl3s= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -70,8 +70,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= -github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= @@ -80,6 +80,8 @@ github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= @@ -139,23 +141,23 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= -golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E= -google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d h1:tUKoKfdZnSjTf5LW7xpG4c6SZ3Ozisn5eumcoTuMEN4= +google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 1995ade..6769741 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -36,6 +36,8 @@ var testDefaultConfig = &AppConfig{ LogRequests: false, EnableReflection: true, EnableInstrumentation: true, + EnableGRPCGateway: true, + GRPCGatewayPath: "/grpc-api", }, OTEL: &OTELConfig{ Enabled: true, @@ -90,6 +92,8 @@ func Test_loadConfig(t *testing.T) { LogRequests: false, EnableReflection: true, EnableInstrumentation: true, + EnableGRPCGateway: true, + GRPCGatewayPath: "/grpc-api", }, OTEL: &OTELConfig{ Enabled: true, @@ -128,6 +132,8 @@ func Test_loadConfig(t *testing.T) { LogRequests: true, EnableReflection: false, EnableInstrumentation: false, + EnableGRPCGateway: true, + GRPCGatewayPath: "/grpc-api", }, OTEL: &OTELConfig{ Enabled: false, diff --git a/pkg/config/types_http.go b/pkg/config/types_http.go index fefc17b..1d7e3b2 100644 --- a/pkg/config/types_http.go +++ b/pkg/config/types_http.go @@ -14,6 +14,8 @@ var defaultHTTPConfig = &HTTPConfig{ ReadTimeout: "10s", WriteTimeout: "10s", IdleTimeout: "1m", + + CORSEnabled: false, } // HTTPConfig provides HTTP server Configuration @@ -25,11 +27,18 @@ type HTTPConfig struct { ReadTimeout string `yaml:"readTimeout" env:"APP_HTTP_READ_TIMEOUT" json:"readTimeout,omitempty"` // Go duration (e.g. 10s) WriteTimeout string `yaml:"writeTimeout" env:"APP_HTTP_WRITE_TIMEOUT" json:"writeTimeout,omitempty"` // Go duration (e.g. 10s) IdleTimeout string `yaml:"idleTimeout" env:"APP_HTTP_IDLE_TIMEOUT" json:"idleTimeout,omitempty"` // Go duration (e.g. 10s) - excludeRegexps []*regexp.Regexp - rT *time.Duration - wT *time.Duration - iT *time.Duration - lock sync.RWMutex + + // CORS configuration + CORSEnabled bool `yaml:"corsEnabled" env:"APP_HTTP_CORS_ENABLED" json:"corsEnabled,omitempty"` + CORSAllowedOrigins []string `yaml:"corsAllowedOrigins" env:"APP_HTTP_CORS_ALLOWED_ORIGINS" json:"corsAllowedOrigins,omitempty"` // Defaults to ["*"] if empty and CORS enabled + CORSAllowCredentials bool `yaml:"corsAllowCredentials" env:"APP_HTTP_CORS_ALLOW_CREDENTIALS" json:"corsAllowCredentials,omitempty"` // Allow credentials (cookies, auth headers) + CORSAllowPrivateNetwork bool `yaml:"corsAllowPrivateNetwork" env:"APP_HTTP_CORS_ALLOW_PRIVATE_NETWORK" json:"corsAllowPrivateNetwork,omitempty"` // Allow requests from private networks + + excludeRegexps []*regexp.Regexp + rT *time.Duration + wT *time.Duration + iT *time.Duration + lock sync.RWMutex } func (h *HTTPConfig) GetExcludeRegexps() []*regexp.Regexp { diff --git a/pkg/srv/http/http.go b/pkg/srv/http/http.go index 794e0b1..4db9e45 100644 --- a/pkg/srv/http/http.go +++ b/pkg/srv/http/http.go @@ -15,6 +15,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/cors" "github.com/rs/zerolog" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/metric" @@ -146,6 +147,29 @@ func prepHTTPServer(opts *opts.AppHTTP) *http.Server { handler = loggingMiddleware(opts.Ctx, handler) } + // Apply CORS middleware if enabled + if cfg.HTTP.CORSEnabled { + allowedOrigins := cfg.HTTP.CORSAllowedOrigins + if len(allowedOrigins) == 0 { + allowedOrigins = []string{"*"} + } + + corsHandler := cors.New(cors.Options{ + AllowedOrigins: allowedOrigins, + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"*"}, + AllowCredentials: cfg.HTTP.CORSAllowCredentials, + AllowPrivateNetwork: cfg.HTTP.CORSAllowPrivateNetwork, + }) + handler = corsHandler.Handler(handler) + + l.Info(). + Strs("allowedOrigins", allowedOrigins). + Bool("allowCredentials", cfg.HTTP.CORSAllowCredentials). + Bool("allowPrivateNetwork", cfg.HTTP.CORSAllowPrivateNetwork). + Msg("CORS enabled") + } + return &http.Server{ Addr: cfg.HTTP.Listen, ReadTimeout: readTimeout,