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 {Database, envToCfg, LinkService, readEnv} from '../src/index.js'
6
7describe.skip('link service', async () => {
8 let linkService: LinkService
9 let baseUrl: string
10 before(async () => {
11 const env = readEnv()
12 const cfg = envToCfg({
13 ...env,
14 hostnames: ['test.bsky.link'],
15 appHostname: 'test.bsky.app',
16 dbPostgresSchema: 'link_test',
17 dbPostgresUrl: process.env.DB_POSTGRES_URL,
18 safelinkEnabled: true,
19 safelinkPdsUrl: 'http://localhost:2583',
20 safelinkAgentIdentifier: 'mod-authority.test',
21 safelinkAgentPass: 'hunter2',
22 })
23 const migrateDb = Database.postgres({
24 url: cfg.db.url,
25 schema: cfg.db.schema,
26 })
27 await migrateDb.migrateToLatestOrThrow()
28 await migrateDb.close()
29 linkService = await LinkService.create(cfg)
30 await linkService.start()
31 const {port} = linkService.server?.address() as AddressInfo
32 baseUrl = `http://localhost:${port}`
33
34 /*
35 // Ensure blocklist, whitelist, and safelink rules are set up
36 const now = new Date().toISOString()
37 linkService.ctx.cfg.eventCache.smartUpdate({
38 $type: 'tools.ozone.safelink.defs#event',
39 id: 1,
40 eventType: ToolsOzoneSafelinkDefs.ADDRULE,
41 url: 'https://en.wikipedia.org/wiki/Fight_Club',
42 pattern: ToolsOzoneSafelinkDefs.URL,
43 action: ToolsOzoneSafelinkDefs.WARN,
44 reason: ToolsOzoneSafelinkDefs.SPAM,
45 createdBy: 'did:example:admin',
46 createdAt: now,
47 comment: 'Do not talk about Fight Club',
48 })
49 linkService.ctx.cfg.eventCache.smartUpdate({
50 $type: 'tools.ozone.safelink.defs#event',
51 id: 2,
52 eventType: ToolsOzoneSafelinkDefs.ADDRULE,
53 url: 'https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a',
54 pattern: ToolsOzoneSafelinkDefs.URL,
55 action: ToolsOzoneSafelinkDefs.BLOCK,
56 reason: ToolsOzoneSafelinkDefs.SPAM,
57 createdBy: 'did:example:admin',
58 createdAt: now,
59 comment: 'All Bs',
60 })
61 linkService.ctx.cfg.eventCache.smartUpdate({
62 $type: 'tools.ozone.safelink.defs#event',
63 id: 3,
64 eventType: ToolsOzoneSafelinkDefs.ADDRULE,
65 url: 'https://en.wikipedia.org',
66 pattern: ToolsOzoneSafelinkDefs.DOMAIN,
67 action: ToolsOzoneSafelinkDefs.WHITELIST,
68 reason: ToolsOzoneSafelinkDefs.NONE,
69 createdBy: 'did:example:admin',
70 createdAt: now,
71 comment: 'Whitelisting the knowledge base of the internet',
72 })
73 linkService.ctx.cfg.eventCache.smartUpdate({
74 $type: 'tools.ozone.safelink.defs#event',
75 id: 4,
76 eventType: ToolsOzoneSafelinkDefs.ADDRULE,
77 url: 'https://www.instagram.com/teamseshbones/?hl=en',
78 pattern: ToolsOzoneSafelinkDefs.URL,
79 action: ToolsOzoneSafelinkDefs.BLOCK,
80 reason: ToolsOzoneSafelinkDefs.SPAM,
81 createdBy: 'did:example:admin',
82 createdAt: now,
83 comment: 'BONES has been erroneously blocked for the sake of this test',
84 })
85 const later = new Date(Date.now() + 1000).toISOString()
86 linkService.ctx.cfg.eventCache.smartUpdate({
87 $type: 'tools.ozone.safelink.defs#event',
88 id: 5,
89 eventType: ToolsOzoneSafelinkDefs.REMOVERULE,
90 url: 'https://www.instagram.com/teamseshbones/?hl=en',
91 pattern: ToolsOzoneSafelinkDefs.URL,
92 action: ToolsOzoneSafelinkDefs.REMOVERULE,
93 reason: ToolsOzoneSafelinkDefs.NONE,
94 createdBy: 'did:example:admin',
95 createdAt: later,
96 comment:
97 'BONES has been resurrected to bring good music to the world once again',
98 })
99 linkService.ctx.cfg.eventCache.smartUpdate({
100 $type: 'tools.ozone.safelink.defs#event',
101 id: 6,
102 eventType: ToolsOzoneSafelinkDefs.ADDRULE,
103 url: 'https://www.leagueoflegends.com/en-us/',
104 pattern: ToolsOzoneSafelinkDefs.URL,
105 action: ToolsOzoneSafelinkDefs.WARN,
106 reason: ToolsOzoneSafelinkDefs.SPAM,
107 createdBy: 'did:example:admin',
108 createdAt: now,
109 comment:
110 'Could be quite the mistake to get into this addicting game, but we will warn instead of block',
111 })
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 /*
217 it('Rule adjustment, safe redirect, 200 response for Instagram Account of teamsesh Bones', async () => {
218 // Retrieve the latest event after all updates
219 const result = linkService.ctx.cfg.eventCache.smartGet(
220 'https://www.instagram.com/teamseshbones/?hl=en',
221 )
222 assert(result, 'Expected event not found in eventCache')
223 assert.strictEqual(result.eventType, ToolsOzoneSafelinkDefs.REMOVERULE)
224 const urlToRedirect = 'https://www.instagram.com/teamseshbones/?hl=en'
225 const url = new URL(`${baseUrl}/redirect`)
226 url.searchParams.set('u', urlToRedirect)
227 const res = await fetch(url, {redirect: 'manual'})
228 assert.strictEqual(res.status, 200)
229 const html = await res.text()
230 assert.match(html, /meta http-equiv="refresh"/)
231 assert.match(
232 html,
233 new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
234 )
235 })
236 */
237
238 async function getRedirect(link: string): Promise<[number, string]> {
239 const url = new URL(link)
240 const base = new URL(baseUrl)
241 url.protocol = base.protocol
242 url.host = base.host
243 const res = await fetch(url, {redirect: 'manual'})
244 await res.arrayBuffer() // drain
245 assert(
246 res.status === 301 || res.status === 303,
247 'response was not a redirect',
248 )
249 return [res.status, res.headers.get('location') ?? '']
250 }
251
252 async function getJsonRedirect(
253 link: string,
254 ): Promise<[number, {url?: string; error?: string; message?: string}]> {
255 const url = new URL(link)
256 const base = new URL(baseUrl)
257 url.protocol = base.protocol
258 url.host = base.host
259 const res = await fetch(url, {
260 redirect: 'manual',
261 headers: {accept: 'application/json,text/html'},
262 })
263 assert(
264 res.headers.get('content-type')?.startsWith('application/json'),
265 'content type was not json',
266 )
267 const json = await res.json()
268 return [res.status, json]
269 }
270
271 async function getLink(path: string): Promise<string> {
272 const res = await fetch(new URL('/link', baseUrl), {
273 method: 'post',
274 headers: {'content-type': 'application/json'},
275 body: JSON.stringify({path}),
276 })
277 assert.strictEqual(res.status, 200)
278 const payload = await res.json()
279 assert(typeof payload.url === 'string')
280 return payload.url
281 }
282})
283
284describe('link service no safelink', async () => {
285 let linkService: LinkService
286 let baseUrl: string
287 before(async () => {
288 const env = readEnv()
289 const cfg = envToCfg({
290 ...env,
291 hostnames: ['test.bsky.link'],
292 appHostname: 'test.bsky.app',
293 dbPostgresSchema: 'link_test',
294 dbPostgresUrl: process.env.DB_POSTGRES_URL,
295 safelinkEnabled: false,
296 safelinkPdsUrl: 'http://localhost:2583',
297 safelinkAgentIdentifier: 'mod-authority.test',
298 safelinkAgentPass: 'hunter2',
299 metricsApiHost: 'http://localhost:2584',
300 })
301 const migrateDb = Database.postgres({
302 url: cfg.db.url,
303 schema: cfg.db.schema,
304 })
305 await migrateDb.migrateToLatestOrThrow()
306 await migrateDb.close()
307 linkService = await LinkService.create(cfg)
308 await linkService.start()
309 const {port} = linkService.server?.address() as AddressInfo
310 baseUrl = `http://localhost:${port}`
311 })
312 after(async () => {
313 await linkService?.destroy()
314 })
315 it('Wikipedia whitelisted, url restricted. Safelink is disabled, so redirect is always safe', async () => {
316 const urlToRedirect = 'https://en.wikipedia.org/wiki/Fight_Club'
317 const url = new URL(`${baseUrl}/redirect`)
318 url.searchParams.set('u', urlToRedirect)
319 const res = await fetch(url, {redirect: 'manual'})
320 assert.strictEqual(res.status, 200)
321 const html = await res.text()
322 assert.match(html, /meta http-equiv="refresh"/)
323 assert.match(
324 html,
325 new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
326 )
327 // No blocked-site div, always safe
328 assert.doesNotMatch(html, /"blocked-site"/)
329 })
330
331 it('Unsafe redirect with block rule, but safelink is disabled so redirect is always safe', async () => {
332 const urlToRedirect =
333 'https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a'
334 const url = new URL(`${baseUrl}/redirect`)
335 url.searchParams.set('u', urlToRedirect)
336 const res = await fetch(url, {redirect: 'manual'})
337 assert.strictEqual(res.status, 200)
338 const html = await res.text()
339 assert.match(html, /meta http-equiv="refresh"/)
340 assert.match(
341 html,
342 new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
343 )
344 // No blocked-site div, always safe
345 assert.doesNotMatch(html, /"blocked-site"/)
346 })
347
348 it('Rule adjustment, safe redirect, safelink is disabled so always safe', async () => {
349 const urlToRedirect = 'https://www.instagram.com/teamseshbones/?hl=en'
350 const url = new URL(`${baseUrl}/redirect`)
351 url.searchParams.set('u', urlToRedirect)
352 const res = await fetch(url, {redirect: 'manual'})
353 assert.strictEqual(res.status, 200)
354 const html = await res.text()
355 assert.match(html, /meta http-equiv="refresh"/)
356 assert.match(
357 html,
358 new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
359 )
360 // No blocked-site div, always safe
361 assert.doesNotMatch(html, /"blocked-site"/)
362 })
363
364 it('normal redirect with query params', async () => {
365 const urlToRedirect = 'https://bsky.app/settings'
366 const url = new URL(`${baseUrl}/redirect`)
367 url.searchParams.set('u', urlToRedirect)
368 url.searchParams.set('utm_source', 'test')
369 const res = await fetch(url, {redirect: 'manual'})
370 assert.strictEqual(res.status, 200)
371 const html = await res.text()
372 assert.match(html, /meta http-equiv="refresh"/)
373 assert.match(
374 html,
375 new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
376 )
377 // No blocked-site div, always safe
378 assert.doesNotMatch(html, /"blocked-site"/)
379 })
380})