Skip to content

Commit d19bd6a

Browse files
authored
feat: Add CORS configuration to dev server API endpoints (#575)
* Add CORS configuration to dev server API endpoints * Fix error handling in cors_test.go for response writing * Update CorsEnabledFlag default value to false * Refactor CORS handling in dev server SDK tests * Add configurable CORS support for dev-server API endpoints * Add CORS settings to config help message
1 parent 595516e commit d19bd6a

File tree

7 files changed

+121
-0
lines changed

7 files changed

+121
-0
lines changed

cmd/cliflags/flags.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const (
88
AccessTokenFlag = "access-token"
99
AnalyticsOptOut = "analytics-opt-out"
1010
BaseURIFlag = "base-uri"
11+
CorsEnabledFlag = "cors-enabled"
12+
CorsOriginFlag = "cors-origin"
1113
DataFlag = "data"
1214
DevStreamURIFlag = "dev-stream-uri"
1315
EmailsFlag = "emails"
@@ -22,6 +24,8 @@ const (
2224
AccessTokenFlagDescription = "LaunchDarkly access token with write-level access"
2325
AnalyticsOptOutDescription = "Opt out of analytics tracking"
2426
BaseURIFlagDescription = "LaunchDarkly base URI"
27+
CorsEnabledFlagDescription = "Enable CORS headers for browser-based developer tools (default: false)"
28+
CorsOriginFlagDescription = "Allowed CORS origin. Use '*' for all origins (default: '*')"
2529
DevStreamURIDescription = "Streaming service endpoint that the dev server uses to obtain authoritative flag data. This may be a LaunchDarkly or Relay Proxy endpoint"
2630
EnvironmentFlagDescription = "Default environment key"
2731
FlagFlagDescription = "Default feature flag key"
@@ -36,6 +40,8 @@ func AllFlagsHelp() map[string]string {
3640
AccessTokenFlag: AccessTokenFlagDescription,
3741
AnalyticsOptOut: AnalyticsOptOutDescription,
3842
BaseURIFlag: BaseURIFlagDescription,
43+
CorsEnabledFlag: CorsEnabledFlagDescription,
44+
CorsOriginFlag: CorsOriginFlagDescription,
3945
DevStreamURIFlag: DevStreamURIDescription,
4046
EnvironmentFlag: EnvironmentFlagDescription,
4147
FlagFlag: FlagFlagDescription,

cmd/config/testdata/help.golden

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Supported settings:
44
- `access-token`: LaunchDarkly access token with write-level access
55
- `analytics-opt-out`: Opt out of analytics tracking
66
- `base-uri`: LaunchDarkly base URI
7+
- `cors-enabled`: Enable CORS headers for browser-based developer tools (default: false)
8+
- `cors-origin`: Allowed CORS origin. Use '*' for all origins (default: '*')
79
- `dev-stream-uri`: Streaming service endpoint that the dev server uses to obtain authoritative flag data. This may be a LaunchDarkly or Relay Proxy endpoint
810
- `environment`: Default environment key
911
- `flag`: Default feature flag key

cmd/dev_server/dev_server.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ func NewDevServerCmd(client resources.Client, analyticsTrackerFn analytics.Track
5050

5151
_ = viper.BindPFlag(cliflags.PortFlag, cmd.PersistentFlags().Lookup(cliflags.PortFlag))
5252

53+
cmd.PersistentFlags().Bool(
54+
cliflags.CorsEnabledFlag,
55+
false,
56+
cliflags.CorsEnabledFlagDescription,
57+
)
58+
_ = viper.BindPFlag(cliflags.CorsEnabledFlag, cmd.PersistentFlags().Lookup(cliflags.CorsEnabledFlag))
59+
60+
cmd.PersistentFlags().String(
61+
cliflags.CorsOriginFlag,
62+
"*",
63+
cliflags.CorsOriginFlagDescription,
64+
)
65+
_ = viper.BindPFlag(cliflags.CorsOriginFlag, cmd.PersistentFlags().Lookup(cliflags.CorsOriginFlag))
66+
5367
// Add subcommands here
5468
cmd.AddGroup(&cobra.Group{ID: "projects", Title: "Project commands:"})
5569
cmd.AddCommand(NewListProjectsCmd(client))

cmd/dev_server/start_server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ func startServer(client dev_server.Client) func(*cobra.Command, []string) error
8989
BaseURI: viper.GetString(cliflags.BaseURIFlag),
9090
DevStreamURI: viper.GetString(cliflags.DevStreamURIFlag),
9191
Port: viper.GetString(cliflags.PortFlag),
92+
CorsEnabled: viper.GetBool(cliflags.CorsEnabledFlag),
93+
CorsOrigin: viper.GetString(cliflags.CorsOriginFlag),
9294
InitialProjectSettings: initialSetting,
9395
}
9496

internal/dev_server/api/cors.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package api
2+
3+
import (
4+
"net/http"
5+
)
6+
7+
// CorsHeadersWithConfig provides configurable CORS support for the dev-server admin API endpoints.
8+
// When enabled=false, no CORS headers are added.
9+
// When enabled=true, CORS headers are added with the specified origin.
10+
func CorsHeadersWithConfig(enabled bool, origin string) func(http.Handler) http.Handler {
11+
return func(handler http.Handler) http.Handler {
12+
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
13+
if enabled {
14+
writer.Header().Set("Access-Control-Allow-Origin", origin)
15+
writer.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
16+
writer.Header().Set("Access-Control-Allow-Credentials", "true")
17+
writer.Header().Set("Access-Control-Allow-Headers", "Accept,Content-Type,Content-Length,Accept-Encoding,Authorization,X-Requested-With")
18+
writer.Header().Set("Access-Control-Expose-Headers", "Date,Content-Length")
19+
writer.Header().Set("Access-Control-Max-Age", "300")
20+
21+
// Handle preflight OPTIONS requests
22+
if request.Method == http.MethodOptions {
23+
writer.WriteHeader(http.StatusOK)
24+
return
25+
}
26+
}
27+
28+
handler.ServeHTTP(writer, request)
29+
})
30+
}
31+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package api
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestCorsHeadersWithConfig_Enabled(t *testing.T) {
13+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14+
w.WriteHeader(http.StatusOK)
15+
_, err := w.Write([]byte("test response"))
16+
require.NoError(t, err)
17+
})
18+
19+
corsHandler := CorsHeadersWithConfig(true, "*")(handler)
20+
21+
// Test GET request
22+
req := httptest.NewRequest("GET", "/dev/projects", nil)
23+
w := httptest.NewRecorder()
24+
corsHandler.ServeHTTP(w, req)
25+
26+
assert.Equal(t, "*", w.Header().Get("Access-Control-Allow-Origin"))
27+
assert.Equal(t, "GET,POST,PUT,PATCH,DELETE,OPTIONS", w.Header().Get("Access-Control-Allow-Methods"))
28+
assert.Equal(t, http.StatusOK, w.Code)
29+
}
30+
31+
func TestCorsHeadersWithConfig_OptionsRequest(t *testing.T) {
32+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33+
t.Error("Handler should not be called for OPTIONS request")
34+
})
35+
36+
corsHandler := CorsHeadersWithConfig(true, "https://example.com")(handler)
37+
38+
// Test OPTIONS preflight request
39+
req := httptest.NewRequest("OPTIONS", "/dev/projects", nil)
40+
w := httptest.NewRecorder()
41+
corsHandler.ServeHTTP(w, req)
42+
43+
assert.Equal(t, "https://example.com", w.Header().Get("Access-Control-Allow-Origin"))
44+
assert.Equal(t, http.StatusOK, w.Code)
45+
}
46+
47+
func TestCorsHeadersWithConfig_Disabled(t *testing.T) {
48+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49+
w.WriteHeader(http.StatusOK)
50+
_, err := w.Write([]byte("test response"))
51+
require.NoError(t, err)
52+
})
53+
54+
corsHandler := CorsHeadersWithConfig(false, "*")(handler)
55+
56+
// Test GET request with CORS disabled
57+
req := httptest.NewRequest("GET", "/dev/projects", nil)
58+
w := httptest.NewRecorder()
59+
corsHandler.ServeHTTP(w, req)
60+
61+
assert.Empty(t, w.Header().Get("Access-Control-Allow-Origin"), "Expected no CORS headers when disabled")
62+
assert.Equal(t, http.StatusOK, w.Code)
63+
}

internal/dev_server/dev_server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ type ServerParams struct {
2929
BaseURI string
3030
DevStreamURI string
3131
Port string
32+
CorsEnabled bool
33+
CorsOrigin string
3234
InitialProjectSettings model.InitialProjectSettings
3335
}
3436

@@ -65,6 +67,7 @@ func (c LDClient) RunServer(ctx context.Context, serverParams ServerParams) {
6567
r.PathPrefix("/ui/").Handler(http.StripPrefix("/ui/", ui.AssetHandler))
6668
sdk.BindRoutes(r)
6769
handler := api.HandlerFromMux(apiServer, r)
70+
handler = api.CorsHeadersWithConfig(serverParams.CorsEnabled, serverParams.CorsOrigin)(handler)
6871
handler = handlers.CombinedLoggingHandler(os.Stdout, handler)
6972
handler = handlers.RecoveryHandler(handlers.PrintRecoveryStack(true))(handler)
7073

0 commit comments

Comments
 (0)