diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index cca86f5b5d0..c5e5556aa76 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -77,6 +77,49 @@ import ( type HeadlampConfig struct { *headlampconfig.HeadlampConfig + proxyURLMu sync.Mutex + compiledProxyURLs []glob.Glob +} + +func compileProxyURLPatterns(patterns []string) ([]glob.Glob, error) { + compiledProxyURLs := make([]glob.Glob, 0, len(patterns)) + + for _, pattern := range patterns { + g, err := glob.Compile(pattern) + if err != nil { + return nil, fmt.Errorf("compiling proxy URL pattern %q: %w", pattern, err) + } + + compiledProxyURLs = append(compiledProxyURLs, g) + } + + return compiledProxyURLs, nil +} + +func (c *HeadlampConfig) proxyURLAllowed(proxyURL string) (bool, error) { + c.proxyURLMu.Lock() + + if c.compiledProxyURLs == nil && len(c.ProxyURLs) > 0 { + compiledProxyURLs, err := compileProxyURLPatterns(c.ProxyURLs) + if err != nil { + c.proxyURLMu.Unlock() + + return false, err + } + + c.compiledProxyURLs = compiledProxyURLs + } + + compiledProxyURLs := c.compiledProxyURLs + c.proxyURLMu.Unlock() + + for _, g := range compiledProxyURLs { + if g.Match(proxyURL) { + return true, nil + } + } + + return false, nil } const DrainNodeCacheTTL = 20 // seconds @@ -601,14 +644,12 @@ func createHeadlampHandler(ctx context.Context, config *HeadlampConfig) http.Han return } - isURLContainedInProxyURLs := false + isURLContainedInProxyURLs, err := config.proxyURLAllowed(url.String()) + if err != nil { + logger.Log(logger.LevelError, map[string]string{"proxyURL": proxyURL}, err, "compiling proxy URL patterns") + http.Error(w, "failed to compile proxy URL patterns", http.StatusInternalServerError) - for _, proxyURL := range config.ProxyURLs { - g := glob.MustCompile(proxyURL) - if g.Match(url.String()) { - isURLContainedInProxyURLs = true - break - } + return } if !isURLContainedInProxyURLs { diff --git a/backend/cmd/headlamp_test.go b/backend/cmd/headlamp_test.go index 662df5bda60..833346eb1e5 100644 --- a/backend/cmd/headlamp_test.go +++ b/backend/cmd/headlamp_test.go @@ -500,6 +500,29 @@ func TestExternalProxy(t *testing.T) { } } +func TestCompileProxyURLPatternsInvalidPattern(t *testing.T) { + _, err := compileProxyURLPatterns([]string{"["}) + + require.Error(t, err) + assert.Contains(t, err.Error(), "compiling proxy URL pattern") +} + +func TestProxyURLAllowedCompilesConfiguredProxyURLs(t *testing.T) { + config := &HeadlampConfig{ + HeadlampConfig: &headlampconfig.HeadlampConfig{ + HeadlampCFG: &headlampconfig.HeadlampCFG{ + ProxyURLs: []string{"https://example.com/*"}, + }, + }, + } + + allowed, err := config.proxyURLAllowed("https://example.com/api") + + require.NoError(t, err) + assert.True(t, allowed) + assert.NotEmpty(t, config.compiledProxyURLs) +} + func TestExternalProxyForwarding(t *testing.T) { // Create a new server for testing that returns a specific status and content type proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/backend/cmd/server.go b/backend/cmd/server.go index 74424afd791..b21dd41a022 100644 --- a/backend/cmd/server.go +++ b/backend/cmd/server.go @@ -98,15 +98,21 @@ func buildHeadlampCFG(conf *config.Config, kubeConfigStore kubeconfig.ContextSto WatchPluginsChanges: conf.WatchPluginsChanges, KubeConfigStore: kubeConfigStore, BaseURL: conf.BaseURL, - ProxyURLs: strings.Split(conf.ProxyURLs, ","), - TLSCertPath: conf.TLSCertPath, - TLSKeyPath: conf.TLSKeyPath, - SessionTTL: conf.SessionTTL, - PodDebugImage: conf.PodDebugImage, - OidcUseCookie: conf.OidcUseCookie, - DefaultLightTheme: conf.DefaultLightTheme, - DefaultDarkTheme: conf.DefaultDarkTheme, - ForceTheme: conf.ForceTheme, + ProxyURLs: func() []string { + if conf.ProxyURLs == "" { + return []string{} + } + + return strings.Split(conf.ProxyURLs, ",") + }(), + TLSCertPath: conf.TLSCertPath, + TLSKeyPath: conf.TLSKeyPath, + SessionTTL: conf.SessionTTL, + PodDebugImage: conf.PodDebugImage, + OidcUseCookie: conf.OidcUseCookie, + DefaultLightTheme: conf.DefaultLightTheme, + DefaultDarkTheme: conf.DefaultDarkTheme, + ForceTheme: conf.ForceTheme, } } @@ -167,8 +173,15 @@ func createHeadlampConfig(conf *config.Config) *HeadlampConfig { cfg.ProxyAuthEmailHeader = conf.ProxyAuthEmailHeader cfg.ProxyAuthTokenHeader = conf.ProxyAuthTokenHeader + compiledProxyURLs, err := compileProxyURLPatterns(cfg.ProxyURLs) + if err != nil { + logger.Log(logger.LevelError, nil, err, "failed to compile proxy URL patterns") + os.Exit(1) + } + return &HeadlampConfig{ - HeadlampConfig: cfg, + HeadlampConfig: cfg, + compiledProxyURLs: compiledProxyURLs, } }