-
-
Notifications
You must be signed in to change notification settings - Fork 114
/
middleware.js
133 lines (116 loc) · 5.83 KB
/
middleware.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import { NextResponse, URLPattern } from 'next/server'
const referrerPattern = new URLPattern({ pathname: ':pathname(*)/r/:referrer([\\w_]+)' })
const itemPattern = new URLPattern({ pathname: '/items/:id(\\d+){/:other(\\w+)}?' })
const profilePattern = new URLPattern({ pathname: '/:name([\\w_]+){/:type(\\w+)}?' })
const territoryPattern = new URLPattern({ pathname: '/~:name([\\w_]+){/*}?' })
// key for /r/... link referrers
const SN_REFERRER = 'sn_referrer'
// we use this to hold /r/... referrers through the redirect
const SN_REFERRER_NONCE = 'sn_referrer_nonce'
// we store the referrers in cookies for a future signup event
// we pass the referrers in the request headers so we can use them in referral rewards for logged in stackers
function referrerMiddleware (request) {
if (referrerPattern.test(request.url)) {
const { pathname, referrer } = referrerPattern.exec(request.url).pathname.groups
const url = new URL(pathname || '/', request.url)
url.search = request.nextUrl.search
url.hash = request.nextUrl.hash
const response = NextResponse.redirect(url)
// explicit referrers are set for a day and can only be overriden by other explicit
// referrers. Content referrers do not override explicit referrers because
// explicit referees might click around before signing up.
response.cookies.set(SN_REFERRER, referrer, { maxAge: 60 * 60 * 24 })
// store the explicit referrer for one page load
// this allows us to attribute both explicit and implicit referrers after the redirect
// e.g. items/<num>/r/<referrer> links should attribute both the item op and the referrer
// without this the /r/<referrer> would be lost on redirect
response.cookies.set(SN_REFERRER_NONCE, referrer, { maxAge: 1 })
return response
}
let contentReferrer
if (itemPattern.test(request.url)) {
let id = request.nextUrl.searchParams.get('commentId')
if (!id) {
({ id } = itemPattern.exec(request.url).pathname.groups)
}
contentReferrer = `item-${id}`
} else if (profilePattern.test(request.url)) {
const { name } = profilePattern.exec(request.url).pathname.groups
contentReferrer = `profile-${name}`
} else if (territoryPattern.test(request.url)) {
const { name } = territoryPattern.exec(request.url).pathname.groups
contentReferrer = `territory-${name}`
}
// pass the referrers to SSR in the request headers
const requestHeaders = new Headers(request.headers)
const referrers = [request.cookies.get(SN_REFERRER_NONCE)?.value, contentReferrer].filter(Boolean)
if (referrers.length) {
requestHeaders.set('x-stacker-news-referrer', referrers.join('; '))
}
const response = NextResponse.next({
request: {
headers: requestHeaders
}
})
// if we don't already have an explicit referrer, give them the content referrer as one
if (!request.cookies.has(SN_REFERRER) && contentReferrer) {
response.cookies.set(SN_REFERRER, contentReferrer, { maxAge: 60 * 60 * 24 })
}
return response
}
export function middleware (request) {
const resp = referrerMiddleware(request)
const isDev = process.env.NODE_ENV === 'development'
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
// we want to load media from other localhost ports during development
const devSrc = isDev ? ' localhost:* http: ws:' : ''
// unsafe-eval is required during development due to react-refresh.js
// see https://github.com/vercel/next.js/issues/14221
const devScriptSrc = isDev ? " 'unsafe-eval'" : ''
const cspHeader = [
// if something is not explicitly allowed, we don't allow it.
"default-src 'self' a.stacker.news",
"font-src 'self' a.stacker.news",
// we want to load images from everywhere but we can limit to HTTPS at least
"img-src 'self' a.stacker.news m.stacker.news https: data: blob:" + devSrc,
"media-src 'self' a.stacker.news m.stacker.news https: blob:" + devSrc,
// Using nonces and strict-dynamic deploys a strict CSP.
// see https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html#strict-policy.
// Old browsers will ignore nonce and strict-dynamic and fallback to host-based matching and unsafe-inline
`script-src 'self' a.stacker.news 'unsafe-inline' 'wasm-unsafe-eval' 'nonce-${nonce}' 'strict-dynamic' https:` + devScriptSrc,
// unsafe-inline for styles is not ideal but okay if script-src is using nonces
"style-src 'self' a.stacker.news 'unsafe-inline'",
"manifest-src 'self'",
'frame-src www.youtube.com platform.twitter.com njump.me open.spotify.com rumble.com embed.wavlake.com bitcointv.com peertube.tv',
"connect-src 'self' https: wss:" + devSrc,
// disable dangerous plugins like Flash
"object-src 'none'",
// blocks injection of <base> tags
"base-uri 'none'",
// tell user agents to replace HTTP with HTTPS
isDev ? '' : 'upgrade-insecure-requests',
// prevents any domain from framing the content (defense against clickjacking attacks)
"frame-ancestors 'none'"
].join('; ')
resp.headers.set('Content-Security-Policy', cspHeader)
// for browsers that don't support CSP
resp.headers.set('X-Frame-Options', 'DENY')
// more useful headers
resp.headers.set('X-Content-Type-Options', 'nosniff')
resp.headers.set('Referrer-Policy', 'origin-when-cross-origin')
resp.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
return resp
}
export const config = {
matcher: [
// NextJS recommends to not add the CSP header to prefetches and static assets
// See https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy
{
source: '/((?!api|_next/static|_error|404|500|offline|_next/image|_next/webpack-hmr|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' }
]
}
]
}