diff --git a/pkg/console/console.go b/pkg/console/console.go index d1d26e654e..092e52a1bb 100644 --- a/pkg/console/console.go +++ b/pkg/console/console.go @@ -90,22 +90,25 @@ func path(u string) (string, error) { } func generateConsoleCSPString(config *Config, nonce string, others ...webui.ContentSecurityPolicy) string { + baseURLs := webui.RewriteSchemes( + webui.WebsocketSchemeRewrites, + config.UI.StackConfig.GS.BaseURL, + config.UI.StackConfig.IS.BaseURL, + config.UI.StackConfig.JS.BaseURL, + config.UI.StackConfig.NS.BaseURL, + config.UI.StackConfig.AS.BaseURL, + config.UI.StackConfig.EDTC.BaseURL, + config.UI.StackConfig.QRG.BaseURL, + config.UI.StackConfig.GCS.BaseURL, + config.UI.StackConfig.DCS.BaseURL, + ) return webui.ContentSecurityPolicy{ - ConnectionSource: []string{ + ConnectionSource: append([]string{ "'self'", - config.UI.StackConfig.GS.BaseURL, - config.UI.StackConfig.IS.BaseURL, - config.UI.StackConfig.JS.BaseURL, - config.UI.StackConfig.NS.BaseURL, - config.UI.StackConfig.AS.BaseURL, - config.UI.StackConfig.EDTC.BaseURL, - config.UI.StackConfig.QRG.BaseURL, - config.UI.StackConfig.GCS.BaseURL, - config.UI.StackConfig.DCS.BaseURL, config.UI.SentryDSN, "gravatar.com", "www.gravatar.com", - }, + }, baseURLs...), StyleSource: []string{ "'self'", config.UI.AssetsBaseURL, diff --git a/pkg/identityserver/identityserver.go b/pkg/identityserver/identityserver.go index 896ed0d2a2..c068ae877b 100644 --- a/pkg/identityserver/identityserver.go +++ b/pkg/identityserver/identityserver.go @@ -88,14 +88,17 @@ func (is *IdentityServer) configFromContext(ctx context.Context) *Config { // GenerateCSPString returns a Content-Security-Policy header value // for OAuth and Account app template. func GenerateCSPString(config *oauth.Config, nonce string) string { + baseURLs := webui.RewriteSchemes( + webui.WebsocketSchemeRewrites, + config.UI.StackConfig.IS.BaseURL, + ) return webui.ContentSecurityPolicy{ - ConnectionSource: []string{ + ConnectionSource: append([]string{ "'self'", - config.UI.StackConfig.IS.BaseURL, config.UI.SentryDSN, "gravatar.com", "www.gravatar.com", - }, + }, baseURLs...), StyleSource: []string{ "'self'", config.UI.AssetsBaseURL, diff --git a/pkg/webui/csp.go b/pkg/webui/csp.go index 30de49f4db..867e3cc1ed 100644 --- a/pkg/webui/csp.go +++ b/pkg/webui/csp.go @@ -66,6 +66,12 @@ func (csp ContentSecurityPolicy) Clean() ContentSecurityPolicy { entry = parsed.Host } } + if strings.HasPrefix(entry, "ws://") || strings.HasPrefix(entry, "wss://") { + if parsed, err := url.Parse(entry); err == nil { + parsed.Path, parsed.RawPath = "", "" + entry = parsed.String() + } + } if _, ok := added[entry]; ok { continue // Skip already added locations. } @@ -112,3 +118,32 @@ func (csp ContentSecurityPolicy) String() string { result = appendPolicy(result, "frame-ancestors", csp.FrameAncestors) return strings.Join(result, " ") } + +// RewriteScheme rewrites the scheme of the provided URL if it matches a rewrite rule. +func RewriteScheme(rewrites map[string]string, baseURL string) []string { + u, err := url.Parse(baseURL) + if err != nil { + return []string{baseURL} + } + rewrite, ok := rewrites[u.Scheme] + if !ok { + return []string{baseURL} + } + u.Scheme = rewrite + return append(make([]string, 0, 2), baseURL, u.String()) +} + +// RewriteSchemes rewrites the scheme of the provided URLs if they match a rewrite rule. +func RewriteSchemes(rewrites map[string]string, baseURLs ...string) []string { + urls := make([]string, 0, 2*len(baseURLs)) + for _, baseURL := range baseURLs { + urls = append(urls, RewriteScheme(rewrites, baseURL)...) + } + return urls +} + +// WebsocketSchemeRewrites contains the rewrite rules for websocket schemes. +var WebsocketSchemeRewrites = map[string]string{ + "http": "ws", + "https": "wss", +} diff --git a/pkg/webui/csp_test.go b/pkg/webui/csp_test.go new file mode 100644 index 0000000000..b3bedb6efe --- /dev/null +++ b/pkg/webui/csp_test.go @@ -0,0 +1,95 @@ +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package webui_test + +import ( + "testing" + + "github.com/smarty/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" + "go.thethings.network/lorawan-stack/v3/pkg/webui" +) + +func TestRewriteScheme(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + baseURL string + rewrites map[string]string + expected []string + }{ + { + name: "no match", + baseURL: "https://example.com", + rewrites: map[string]string{ + "http": "ws", + }, + expected: []string{"https://example.com"}, + }, + { + name: "match", + baseURL: "https://example.com", + rewrites: map[string]string{ + "https": "wss", + }, + expected: []string{"https://example.com", "wss://example.com"}, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + a := assertions.New(t) + actual := webui.RewriteScheme(tc.rewrites, tc.baseURL) + a.So(actual, should.Resemble, tc.expected) + }) + } +} + +func TestRewriteSchemes(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + baseURLs []string + rewrites map[string]string + expected []string + }{ + { + name: "no match", + baseURLs: []string{"https://foo.example.com", "https://bar.example.com"}, + rewrites: map[string]string{ + "http": "ws", + }, + expected: []string{"https://foo.example.com", "https://bar.example.com"}, + }, + { + name: "match", + baseURLs: []string{"https://foo.example.com", "https://bar.example.com"}, + rewrites: map[string]string{ + "https": "wss", + }, + expected: []string{ + "https://foo.example.com", "wss://foo.example.com", "https://bar.example.com", "wss://bar.example.com", + }, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + a := assertions.New(t) + actual := webui.RewriteSchemes(tc.rewrites, tc.baseURLs...) + a.So(actual, should.Resemble, tc.expected) + }) + } +}