122 lines
3.3 KiB
JavaScript
122 lines
3.3 KiB
JavaScript
/* eslint-disable @next/next/no-server-import-in-page */
|
|||
|
|
import {NextResponse} from 'next/server'
|
||
|
|
|
||
|
|
const nonceCharset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||
|
|
|
||
|
|
function generateNonce() {
|
||
|
|
const array = new Uint8Array(16)
|
||
|
|
crypto.getRandomValues(array)
|
||
|
|
let nonce = ''
|
||
|
|
for (const byte of array) {
|
||
|
|
nonce += nonceCharset[byte % 64]
|
||
|
|
}
|
||
|
|
|
||
|
|
return nonce
|
||
|
|
}
|
||
|
|
|
||
|
|
function extractOrigin(url) {
|
||
|
|
if (!url) {
|
||
|
|
return ''
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
return new URL(url).origin
|
||
|
|
} catch {
|
||
|
|
return ''
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function extractHostSource(raw) {
|
||
|
|
if (!raw) {
|
||
|
|
return ''
|
||
|
|
}
|
||
|
|
|
||
|
|
const [hostname, port] = raw.split(':')
|
||
|
|
if (!hostname) {
|
||
|
|
return ''
|
||
|
|
}
|
||
|
|
|
||
|
|
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||
|
|
return `http://${hostname}${port ? `:${port}` : ''}`
|
||
|
|
}
|
||
|
|
|
||
|
|
return `https://${hostname}${port ? `:${port}` : ''}`
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildCsp(nonce) {
|
||
|
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL || process.env.NEXT_PUBLIC_API_URL_ROOT || ''
|
||
|
|
const plausibleUrl = process.env.NEXT_PUBLIC_PLAUSIBLE_URL || ''
|
||
|
|
const mizikUrl = process.env.NEXT_PUBLIC_OKI_MIZIK_URL || ''
|
||
|
|
const imageDomainsRaw = process.env.NEXT_PUBLIC_DOMAINS_IMAGE || ''
|
||
|
|
|
||
|
|
const apiOrigin = extractOrigin(apiUrl)
|
||
|
|
const plausibleOrigin = extractOrigin(plausibleUrl)
|
||
|
|
const mizikOrigin = extractOrigin(mizikUrl)
|
||
|
|
const imageOrigins = imageDomainsRaw
|
||
|
|
.split(' ')
|
||
|
|
.filter(Boolean)
|
||
|
|
.map(entry => extractHostSource(entry))
|
||
|
|
.filter(Boolean)
|
||
|
|
|
||
|
|
const imageOriginsSet = new Set([apiOrigin, ...imageOrigins])
|
||
|
|
const imageOriginsUnique = [...imageOriginsSet].filter(Boolean)
|
||
|
|
|
||
|
|
const connectSrc = ['\'self\'', apiOrigin, plausibleOrigin, mizikOrigin].filter(Boolean)
|
||
|
|
const imgSrc = ['\'self\'', 'data:', 'blob:', ...imageOriginsUnique].filter(Boolean)
|
||
|
|
const mediaSrc = ['\'self\'', apiOrigin, mizikOrigin].filter(Boolean)
|
||
|
|
|
||
|
|
// Frame-src is intentionally broad: the application embeds user-supplied
|
||
|
|
// PeerTube instances in addition to known platforms (Tidal, Deezer, Spotify,
|
||
|
|
// Soundcloud, Apple Music). Restricting this to a fixed list would break
|
||
|
|
// user-generated content.
|
||
|
|
const frameSrc = ['\'self\'', 'https:']
|
||
|
|
|
||
|
|
const directives = [
|
||
|
|
'default-src \'self\'',
|
||
|
|
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
|
||
|
|
`style-src-elem 'self' 'nonce-${nonce}'`,
|
||
|
|
'style-src-attr \'unsafe-inline\'',
|
||
|
|
`img-src ${imgSrc.join(' ')}`,
|
||
|
|
'font-src \'self\'',
|
||
|
|
`connect-src ${connectSrc.join(' ')}`,
|
||
|
|
`frame-src ${frameSrc.join(' ')}`,
|
||
|
|
`media-src ${mediaSrc.join(' ')}`,
|
||
|
|
'manifest-src \'self\'',
|
||
|
|
'object-src \'none\'',
|
||
|
|
'base-uri \'self\'',
|
||
|
|
'form-action \'self\'',
|
||
|
|
'frame-ancestors \'self\'',
|
||
|
|
'upgrade-insecure-requests'
|
||
|
|
]
|
||
|
|
|
||
|
|
return directives.join('; ')
|
||
|
|
}
|
||
|
|
|
||
|
|
export function middleware(request) {
|
||
|
|
const nonce = generateNonce()
|
||
|
|
const requestHeaders = new Headers(request.headers)
|
||
|
|
requestHeaders.set('x-nonce', nonce)
|
||
|
|
|
||
|
|
const response = NextResponse.next({
|
||
|
|
request: {
|
||
|
|
headers: requestHeaders
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
response.headers.set('Content-Security-Policy', buildCsp(nonce))
|
||
|
|
|
||
|
|
return response
|
||
|
|
}
|
||
|
|
|
||
|
|
export const config = {
|
||
|
|
matcher: [
|
||
|
|
{
|
||
|
|
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
|
||
|
|
missing: [
|
||
|
|
{type: 'header', key: 'next-router-prefetch'},
|
||
|
|
{type: 'header', key: 'purpose', value: 'prefetch'}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|