Bluesky app fork with some witchin' additions 馃挮
at main 360 lines 13 kB view raw
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})