Files
GoToSocial/internal/middleware/contentsecuritypolicy.go
tobi 00e58c60cd [feature] Add ListenBrainz functionality on the web view (#4184)
This pull request adds a very simple ad-hoc ListenBrainz widget to the frontend web view, with progressive enhancement (in all fail states it just falls back to rendering the field as normal).

This necessitated adding the ListenBrainz API endpoint to the `connect-src` part of our Content-Security-Policy header. We might want to tweak this to only add that endpoint to `connect-src` for profiles, and then only for profiles that include a ListenBrainz field, but this would require significant dicking about with the middleware, and checks inside the app logic, such that it might not be worthwhile (after all, we control all the scripts right now anyway).

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4184
Co-authored-by: tobi <tobi.smethurst@protonmail.com>
Co-committed-by: tobi <tobi.smethurst@protonmail.com>
2025-05-22 12:34:39 +02:00

166 lines
4.1 KiB
Go

// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package middleware
import (
"strings"
"codeberg.org/gruf/go-debug"
"github.com/gin-gonic/gin"
)
func ContentSecurityPolicy(extraURIs ...string) gin.HandlerFunc {
csp := BuildContentSecurityPolicy(extraURIs...)
return func(c *gin.Context) {
// Inform the browser we only load
// CSS/JS/media using the given policy.
c.Header("Content-Security-Policy", csp)
}
}
func BuildContentSecurityPolicy(extraURIs ...string) string {
const (
defaultSrc = "default-src"
connectSrc = "connect-src"
objectSrc = "object-src"
imgSrc = "img-src"
mediaSrc = "media-src"
frames = "frame-ancestors"
self = "'self'"
none = "'none'"
blob = "blob:"
)
// CSP values keyed by directive.
values := make(map[string][]string, 5)
/*
default-src
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src
*/
if !debug.DEBUG {
// Restrictive 'self' policy
values[defaultSrc] = []string{self}
} else {
// If debug is enabled, allow
// serving things from localhost
// as well (regardless of port).
values[defaultSrc] = []string{
self,
"localhost:*",
"ws://localhost:*",
}
}
/*
connect-src
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src
*/
// Restrictive default policy, but
// include ListenBrainz API for fields.
const listenBrains = "https://api.listenbrainz.org/1/user/"
values[connectSrc] = append(values[defaultSrc], listenBrains) //nolint
/*
object-src
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/object-src
*/
// Disallow object-src as recommended.
values[objectSrc] = []string{none}
/*
img-src
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/img-src
*/
// Restrictive 'self' policy,
// include extraURIs, and 'blob:'
// for previewing uploaded images
// (header, avi, emojis) in settings.
values[imgSrc] = append(
[]string{self, blob},
extraURIs...,
)
/*
media-src
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/media-src
*/
// Restrictive 'self' policy,
// include extraURIs.
values[mediaSrc] = append(
[]string{self},
extraURIs...,
)
/*
frame-ancestors
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors
*/
// Don't allow embedding us in an iframe
values[frames] = []string{none}
/*
Assemble policy directives.
*/
// Iterate through an ordered slice rather than
// iterating through the map, since we want these
// policyDirectives in a determinate order.
policyDirectives := make([]string, 5)
for i, directive := range []string{
defaultSrc,
connectSrc,
objectSrc,
imgSrc,
mediaSrc,
} {
// Each policy directive should look like:
// `[directive] [value1] [value2] [etc]`
// Get assembled values
// for this directive.
values := values[directive]
// Prepend values with
// the directive name.
directiveValues := append(
[]string{directive},
values...,
)
// Space-separate them.
policyDirective := strings.Join(directiveValues, " ")
// Done.
policyDirectives[i] = policyDirective
}
// Content-security-policy looks like this:
// `Content-Security-Policy: <policy-directive>; <policy-directive>`
// So join each policy directive appropriately.
return strings.Join(policyDirectives, "; ")
}