50 lines
3.3 KiB
JavaScript
50 lines
3.3 KiB
JavaScript
import { expect, test } from 'bun:test';
|
|
import { Hono } from 'hono';
|
|
import { privateAddress, sanitize, extractArticle, registerReaderRoutes } from './reader.js';
|
|
|
|
const PAGE = `<!doctype html><html><head><title>Psalm 23 study</title></head><body>
|
|
<nav>menu menu menu</nav>
|
|
<article><h1>Psalm 23 study</h1><p class="by">By Ana</p>
|
|
${'<p>The Lord is my shepherd; I shall not want. He makes me lie down in green pastures. He leads me beside still waters. </p>'.repeat(8)}
|
|
<p>Read <a href="/next" onclick="steal()">the next part</a> <img src="/pic.jpg" onerror="x()"> <script>alert(1)</script></p>
|
|
<iframe src="https://evil.example"></iframe><a href="javascript:alert(1)">bad</a></article></body></html>`;
|
|
|
|
test('private and special addresses are refused', () => {
|
|
for (const ip of ['127.0.0.1', '10.8.0.1', '192.168.1.2', '172.20.0.1', '169.254.169.254', '100.64.0.1', '::1', 'fd00::1', '::ffff:10.0.0.1', '0.0.0.0'])
|
|
expect(privateAddress(ip)).toBe(true);
|
|
for (const ip of ['93.184.216.34', '1.1.1.1', '2606:4700::1111']) expect(privateAddress(ip)).toBe(false);
|
|
});
|
|
|
|
test('sanitize keeps reading tags, makes links absolute and strips scripts/handlers', () => {
|
|
const out = sanitize('<p style="x" onclick="a()">Hi <a href="/x">link</a><script>bad()</script><img src="i.png" onerror="y()"><iframe></iframe><custom>kept text</custom></p>', 'https://site.example/a/b');
|
|
expect(out).toMatch(/<a [^>]*href="https:\/\/site\.example\/x"[^>]*>link<\/a>/);
|
|
expect(out).toContain('rel="noopener noreferrer nofollow"');
|
|
expect(out).toContain('src="https://site.example/a/i.png"');
|
|
expect(out).toContain('kept text');
|
|
for (const bad of ['script', 'onclick', 'onerror', 'iframe', 'style=', 'custom']) expect(out).not.toContain(bad);
|
|
});
|
|
|
|
test('extractArticle finds the article and drops unsafe parts', () => {
|
|
const a = extractArticle(PAGE, 'https://bible.example/psalm-23');
|
|
expect(a.title).toBe('Psalm 23 study');
|
|
expect(a.content).toContain('The Lord is my shepherd');
|
|
expect(a.content).toContain('href="https://bible.example/next"');
|
|
for (const bad of ['<script', 'onclick', 'onerror', 'javascript:', 'iframe']) expect(a.content).not.toContain(bad);
|
|
});
|
|
|
|
test('the route reads only the presented link, refuses private targets and redirects into them', async () => {
|
|
const present = { ABCDEF: 'https://bible.example/psalm-23', PRIVAT: 'http://10.0.0.5/admin', HOPPER: 'https://hop.example/' };
|
|
const resolve = async (h) => [{ address: h === 'inside.example' ? '192.168.0.9' : '93.184.216.34' }];
|
|
const fetchImpl = async (url) => url.startsWith('https://hop.example')
|
|
? new Response('', { status: 302, headers: { location: 'http://inside.example/secret' } })
|
|
: new Response(PAGE, { headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
const app = new Hono();
|
|
registerReaderRoutes(app, { presentUrlOf: (code) => present[code], fetchImpl, resolve });
|
|
const get = async (code) => { const r = await app.request(`/api/party/${code}/reader`); return { status: r.status, json: await r.json() }; };
|
|
expect((await get('ZZZZZZ')).status).toBe(404);
|
|
const ok = await get('abcdef');
|
|
expect(ok.json).toMatchObject({ ok: true, title: 'Psalm 23 study' });
|
|
expect((await get('PRIVAT')).json.error).toMatch(/public internet/);
|
|
expect((await get('HOPPER')).json.error).toMatch(/public internet/);
|
|
});
|