forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import assert from 'node:assert'
2import {type AddressInfo} from 'node:net'
3import {after, before, describe, it} from 'node:test'
4
5import {ToolsOzoneSafelinkDefs} from '@atproto/api'
6
7import {Database, envToCfg, LinkService, readEnv} from '../src/index.js'
8
9describe('link service', async () => {
10 let linkService: LinkService
11 let baseUrl: string
12 before(async () => {
13 const env = readEnv()
14 const cfg = envToCfg({
15 ...env,
16 hostnames: ['test.bsky.link'],
17 appHostname: 'test.bsky.app',
18 dbPostgresSchema: 'link_test',
19 dbPostgresUrl: process.env.DB_POSTGRES_URL,
20 safelinkEnabled: true,
21 ozoneUrl: 'http://localhost:2583',
22 ozoneAgentHandle: 'mod-authority.test',
23 ozoneAgentPass: 'hunter2',
24 })
25 const migrateDb = Database.postgres({
26 url: cfg.db.url,
27 schema: cfg.db.schema,
28 })
29 await migrateDb.migrateToLatestOrThrow()
30 await migrateDb.close()
31 linkService = await LinkService.create(cfg)
32 await linkService.start()
33 const {port} = linkService.server?.address() as AddressInfo
34 baseUrl = `http://localhost:${port}`
35
36 // Ensure blocklist, whitelist, and safelink rules are set up
37 const now = new Date().toISOString()
38 linkService.ctx.cfg.eventCache.smartUpdate({
39 $type: 'tools.ozone.safelink.defs#event',
40 id: 1,
41 eventType: ToolsOzoneSafelinkDefs.ADDRULE,
42 url: 'https://en.wikipedia.org/wiki/Fight_Club',
43 pattern: ToolsOzoneSafelinkDefs.URL,
44 action: ToolsOzoneSafelinkDefs.WARN,
45 reason: ToolsOzoneSafelinkDefs.SPAM,
46 createdBy: 'did:example:admin',
47 createdAt: now,
48 comment: 'Do not talk about Fight Club',
49 })
50 linkService.ctx.cfg.eventCache.smartUpdate({
51 $type: 'tools.ozone.safelink.defs#event',
52 id: 2,
53 eventType: ToolsOzoneSafelinkDefs.ADDRULE,
54 url: 'https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a',
55 pattern: ToolsOzoneSafelinkDefs.URL,
56 action: ToolsOzoneSafelinkDefs.BLOCK,
57 reason: ToolsOzoneSafelinkDefs.SPAM,
58 createdBy: 'did:example:admin',
59 createdAt: now,
60 comment: 'All Bs',
61 })
62 linkService.ctx.cfg.eventCache.smartUpdate({
63 $type: 'tools.ozone.safelink.defs#event',
64 id: 3,
65 eventType: ToolsOzoneSafelinkDefs.ADDRULE,
66 url: 'https://en.wikipedia.org',
67 pattern: ToolsOzoneSafelinkDefs.DOMAIN,
68 action: ToolsOzoneSafelinkDefs.WHITELIST,
69 reason: ToolsOzoneSafelinkDefs.NONE,
70 createdBy: 'did:example:admin',
71 createdAt: now,
72 comment: 'Whitelisting the knowledge base of the internet',
73 })
74 linkService.ctx.cfg.eventCache.smartUpdate({
75 $type: 'tools.ozone.safelink.defs#event',
76 id: 4,
77 eventType: ToolsOzoneSafelinkDefs.ADDRULE,
78 url: 'https://www.instagram.com/teamseshbones/?hl=en',
79 pattern: ToolsOzoneSafelinkDefs.URL,
80 action: ToolsOzoneSafelinkDefs.BLOCK,
81 reason: ToolsOzoneSafelinkDefs.SPAM,
82 createdBy: 'did:example:admin',
83 createdAt: now,
84 comment: 'BONES has been erroneously blocked for the sake of this test',
85 })
86 const later = new Date(Date.now() + 1000).toISOString()
87 linkService.ctx.cfg.eventCache.smartUpdate({
88 $type: 'tools.ozone.safelink.defs#event',
89 id: 5,
90 eventType: ToolsOzoneSafelinkDefs.REMOVERULE,
91 url: 'https://www.instagram.com/teamseshbones/?hl=en',
92 pattern: ToolsOzoneSafelinkDefs.URL,
93 action: ToolsOzoneSafelinkDefs.REMOVERULE,
94 reason: ToolsOzoneSafelinkDefs.NONE,
95 createdBy: 'did:example:admin',
96 createdAt: later,
97 comment:
98 'BONES has been resurrected to bring good music to the world once again',
99 })
100 linkService.ctx.cfg.eventCache.smartUpdate({
101 $type: 'tools.ozone.safelink.defs#event',
102 id: 6,
103 eventType: ToolsOzoneSafelinkDefs.ADDRULE,
104 url: 'https://www.leagueoflegends.com/en-us/',
105 pattern: ToolsOzoneSafelinkDefs.URL,
106 action: ToolsOzoneSafelinkDefs.WARN,
107 reason: ToolsOzoneSafelinkDefs.SPAM,
108 createdBy: 'did:example:admin',
109 createdAt: now,
110 comment:
111 'Could be quite the mistake to get into this addicting game, but we will warn instead of block',
112 })
113 })
114 after(async () => {
115 await linkService?.destroy()
116 })
117
118 it('creates a starter pack link', async () => {
119 const link = await getLink('/start/did:example:alice/xxx')
120 const url = new URL(link)
121 assert.strictEqual(url.origin, 'https://test.bsky.link')
122 assert.match(url.pathname, /^\/[a-z0-9]+$/i)
123 })
124
125 it('normalizes input paths and provides same link each time.', async () => {
126 const link1 = await getLink('/start/did%3Aexample%3Abob/yyy')
127 const link2 = await getLink('/start/did:example:bob/yyy/')
128 assert.strictEqual(link1, link2)
129 })
130
131 it('serves permanent redirect, preserving query params.', async () => {
132 const link = await getLink('/start/did:example:carol/zzz/')
133 const [status, location] = await getRedirect(`${link}?a=b`)
134 assert.strictEqual(status, 301)
135 const locationUrl = new URL(location)
136 assert.strictEqual(
137 locationUrl.pathname + locationUrl.search,
138 '/start/did:example:carol/zzz?a=b',
139 )
140 })
141
142 it('returns json object with url when requested', async () => {
143 const link = await getLink('/start/did:example:carol/zzz/')
144 const [status, json] = await getJsonRedirect(link)
145 assert.strictEqual(status, 200)
146 assert(json.url)
147 const url = new URL(json.url)
148 assert.strictEqual(url.pathname, '/start/did:example:carol/zzz')
149 })
150
151 it('returns 404 for unknown link when requesting json', async () => {
152 const [status, json] = await getJsonRedirect(
153 'https://test.bsky.link/unknown',
154 )
155 assert(json.error)
156 assert(json.message)
157 assert.strictEqual(status, 404)
158 assert.strictEqual(json.error, 'NotFound')
159 assert.strictEqual(json.message, 'Link not found')
160 })
161
162 it('League of Legends warned', async () => {
163 const urlToRedirect = 'https://www.leagueoflegends.com/en-us/'
164 const url = new URL(`${baseUrl}/redirect`)
165 url.searchParams.set('u', urlToRedirect)
166 const res = await fetch(url, {redirect: 'manual'})
167 assert.strictEqual(res.status, 200)
168 const html = await res.text()
169 assert.match(
170 html,
171 new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
172 )
173 // League of Legends is set to WARN, not BLOCK, so expect a warning (blocked-site div present)
174 assert.match(
175 html,
176 /Warning: Malicious Link/,
177 'Expected warning not found in HTML',
178 )
179 })
180
181 it('Wikipedia whitelisted, url restricted. Redirect safely since wikipedia is whitelisted', async () => {
182 const urlToRedirect = 'https://en.wikipedia.org/wiki/Fight_Club'
183 const url = new URL(`${baseUrl}/redirect`)
184 url.searchParams.set('u', urlToRedirect)
185 const res = await fetch(url, {redirect: 'manual'})
186 assert.strictEqual(res.status, 200)
187 const html = await res.text()
188 assert.match(html, /meta http-equiv="refresh"/)
189 assert.match(
190 html,
191 new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
192 )
193 // Wikipedia domain is whitelisted, so no blocked-site div should be present
194 assert.doesNotMatch(html, /"blocked-site"/)
195 })
196
197 it('Unsafe redirect with block rule, due to the content of webpage.', async () => {
198 const urlToRedirect =
199 'https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a'
200 const url = new URL(`${baseUrl}/redirect`)
201 url.searchParams.set('u', urlToRedirect)
202 const res = await fetch(url, {redirect: 'manual'})
203 assert.strictEqual(res.status, 200)
204 const html = await res.text()
205 assert.match(
206 html,
207 new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
208 )
209 assert.match(
210 html,
211 /"blocked-site"/,
212 'Expected blocked-site div not found in HTML',
213 )
214 })
215
216 it('Rule adjustment, safe redirect, 200 response for Instagram Account of teamsesh Bones', async () => {
217 // Retrieve the latest event after all updates
218 const result = linkService.ctx.cfg.eventCache.smartGet(
219 'https://www.instagram.com/teamseshbones/?hl=en',
220 )
221 assert(result, 'Expected event not found in eventCache')
222 assert.strictEqual(result.eventType, ToolsOzoneSafelinkDefs.REMOVERULE)
223 const urlToRedirect = 'https://www.instagram.com/teamseshbones/?hl=en'
224 const url = new URL(`${baseUrl}/redirect`)
225 url.searchParams.set('u', urlToRedirect)
226 const res = await fetch(url, {redirect: 'manual'})
227 assert.strictEqual(res.status, 200)
228 const html = await res.text()
229 assert.match(html, /meta http-equiv="refresh"/)
230 assert.match(
231 html,
232 new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
233 )
234 })
235
236 async function getRedirect(link: string): Promise<[number, string]> {
237 const url = new URL(link)
238 const base = new URL(baseUrl)
239 url.protocol = base.protocol
240 url.host = base.host
241 const res = await fetch(url, {redirect: 'manual'})
242 await res.arrayBuffer() // drain
243 assert(
244 res.status === 301 || res.status === 303,
245 'response was not a redirect',
246 )
247 return [res.status, res.headers.get('location') ?? '']
248 }
249
250 async function getJsonRedirect(
251 link: string,
252 ): Promise<[number, {url?: string; error?: string; message?: string}]> {
253 const url = new URL(link)
254 const base = new URL(baseUrl)
255 url.protocol = base.protocol
256 url.host = base.host
257 const res = await fetch(url, {
258 redirect: 'manual',
259 headers: {accept: 'application/json,text/html'},
260 })
261 assert(
262 res.headers.get('content-type')?.startsWith('application/json'),
263 'content type was not json',
264 )
265 const json = await res.json()
266 return [res.status, json]
267 }
268
269 async function getLink(path: string): Promise<string> {
270 const res = await fetch(new URL('/link', baseUrl), {
271 method: 'post',
272 headers: {'content-type': 'application/json'},
273 body: JSON.stringify({path}),
274 })
275 assert.strictEqual(res.status, 200)
276 const payload = await res.json()
277 assert(typeof payload.url === 'string')
278 return payload.url
279 }
280})
281
282describe('link service no safelink', async () => {
283 let linkService: LinkService
284 let baseUrl: string
285 before(async () => {
286 const env = readEnv()
287 const cfg = envToCfg({
288 ...env,
289 hostnames: ['test.bsky.link'],
290 appHostname: 'test.bsky.app',
291 dbPostgresSchema: 'link_test',
292 dbPostgresUrl: process.env.DB_POSTGRES_URL,
293 safelinkEnabled: false,
294 ozoneUrl: 'http://localhost:2583',
295 ozoneAgentHandle: 'mod-authority.test',
296 ozoneAgentPass: 'hunter2',
297 })
298 const migrateDb = Database.postgres({
299 url: cfg.db.url,
300 schema: cfg.db.schema,
301 })
302 await migrateDb.migrateToLatestOrThrow()
303 await migrateDb.close()
304 linkService = await LinkService.create(cfg)
305 await linkService.start()
306 const {port} = linkService.server?.address() as AddressInfo
307 baseUrl = `http://localhost:${port}`
308 })
309 after(async () => {
310 await linkService?.destroy()
311 })
312 it('Wikipedia whitelisted, url restricted. Safelink is disabled, so redirect is always safe', async () => {
313 const urlToRedirect = 'https://en.wikipedia.org/wiki/Fight_Club'
314 const url = new URL(`${baseUrl}/redirect`)
315 url.searchParams.set('u', urlToRedirect)
316 const res = await fetch(url, {redirect: 'manual'})
317 assert.strictEqual(res.status, 200)
318 const html = await res.text()
319 assert.match(html, /meta http-equiv="refresh"/)
320 assert.match(
321 html,
322 new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
323 )
324 // No blocked-site div, always safe
325 assert.doesNotMatch(html, /"blocked-site"/)
326 })
327
328 it('Unsafe redirect with block rule, but safelink is disabled so redirect is always safe', async () => {
329 const urlToRedirect =
330 'https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a'
331 const url = new URL(`${baseUrl}/redirect`)
332 url.searchParams.set('u', urlToRedirect)
333 const res = await fetch(url, {redirect: 'manual'})
334 assert.strictEqual(res.status, 200)
335 const html = await res.text()
336 assert.match(html, /meta http-equiv="refresh"/)
337 assert.match(
338 html,
339 new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
340 )
341 // No blocked-site div, always safe
342 assert.doesNotMatch(html, /"blocked-site"/)
343 })
344
345 it('Rule adjustment, safe redirect, safelink is disabled so always safe', async () => {
346 const urlToRedirect = 'https://www.instagram.com/teamseshbones/?hl=en'
347 const url = new URL(`${baseUrl}/redirect`)
348 url.searchParams.set('u', urlToRedirect)
349 const res = await fetch(url, {redirect: 'manual'})
350 assert.strictEqual(res.status, 200)
351 const html = await res.text()
352 assert.match(html, /meta http-equiv="refresh"/)
353 assert.match(
354 html,
355 new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
356 )
357 // No blocked-site div, always safe
358 assert.doesNotMatch(html, /"blocked-site"/)
359 })
360})