forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import assert from 'node:assert'
2
3import {DAY, SECOND} from '@atproto/common'
4import {type Express} from 'express'
5
6import {type AppContext} from '../context.js'
7import {linkRedirectContents} from '../html/linkRedirectContents.js'
8import {linkWarningContents} from '../html/linkWarningContents.js'
9import {linkWarningLayout} from '../html/linkWarningLayout.js'
10import {redirectLogger} from '../logger.js'
11import {handler} from './util.js'
12
13const INTERNAL_IP_REGEX = new RegExp(
14 '(^127.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$)|(^10.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.1[6-9]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.2[0-9]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.3[0-1]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^192.168.[0-9]{1,3}.[0-9]{1,3}$)|^localhost',
15 'i',
16)
17
18export default function (ctx: AppContext, app: Express) {
19 return app.get(
20 '/redirect',
21 handler(async (req, res) => {
22 let link = req.query.u
23 assert(
24 typeof link === 'string',
25 'express guarantees link query parameter is a string',
26 )
27
28 let url: URL | undefined
29 try {
30 url = new URL(link)
31 } catch {}
32
33 if (
34 !url ||
35 (url.protocol !== 'http:' && url.protocol !== 'https:') || // is a http(s) url
36 (ctx.cfg.service.hostnames.includes(url.hostname.toLowerCase()) &&
37 url.pathname === '/redirect') || // is a redirect loop
38 INTERNAL_IP_REGEX.test(url.hostname) // isn't directing to an internal location
39 ) {
40 res.setHeader('Cache-Control', 'no-store')
41 res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
42 return res.status(302).end()
43 }
44
45 // Default to a max age header
46 res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`)
47 res.status(200)
48 res.type('html')
49
50 let html: string | undefined
51
52 if (ctx.cfg.service.safelinkEnabled) {
53 const rule = await ctx.safelinkClient.tryFindRule(link)
54 if (rule !== 'ok') {
55 switch (rule.action) {
56 case 'whitelist':
57 redirectLogger.info({rule}, 'Whitelist rule matched')
58 break
59 case 'block':
60 html = linkWarningLayout(
61 'Blocked Link Warning',
62 linkWarningContents(req, {
63 type: 'block',
64 link: url.href,
65 }),
66 )
67 res.setHeader('Cache-Control', 'no-store')
68 redirectLogger.info({rule}, 'Block rule matched')
69 break
70 case 'warn':
71 html = linkWarningLayout(
72 'Malicious Link Warning',
73 linkWarningContents(req, {
74 type: 'warn',
75 link: url.href,
76 }),
77 )
78 res.setHeader('Cache-Control', 'no-store')
79 redirectLogger.info({rule}, 'Warn rule matched')
80 break
81 default:
82 redirectLogger.warn({rule}, 'Unknown rule matched')
83 }
84 }
85 }
86
87 // If there is no html defined yet, we will create a redirect html
88 if (!html) {
89 html = linkRedirectContents(url.href)
90 }
91
92 return res.end(html)
93 }),
94 )
95}