Files
ink-note-app/test/linkpreview.test.js
2026-10-15 11:57:09 +02:00

80 lines
5.3 KiB
JavaScript

// Web link previews: reading a page's title and description, and never fetching private addresses.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const { parsePreview, fetchPreview, allowed } = require('../electron/linkpreview.cjs');
const page = (head) => `<!doctype html><html><head>${head}</head><body><p>hi</p></body></html>`;
const reply = (body, { status = 200, type = 'text/html; charset=utf-8', headers = {} } = {}) => ({
ok: status >= 200 && status < 300,
status,
headers: new Headers({ 'content-type': type, ...headers }),
body: new Response(body).body
});
const PUBLIC = async () => [{ address: '93.184.216.34' }];
test('parsePreview prefers Open Graph, falls back to <title> and the description meta', () => {
assert.deepEqual(
parsePreview(page('<title>Plain</title><meta property="og:title" content="The &amp; Title"><meta name="description" content="Desc"><meta property="og:description" content="OG &quot;desc&quot;"><meta property="og:site_name" content="Site">')),
{ title: 'The & Title', description: 'OG "desc"', site: 'Site' }
);
assert.deepEqual(parsePreview(page('<title>\n Just a\n title </title><meta name="description" content="D">')), { title: 'Just a title', description: 'D', site: '' });
assert.deepEqual(parsePreview(page("<meta content='single' name='twitter:title'><meta content=bare name=description>")), { title: 'single', description: 'bare', site: '' });
assert.deepEqual(parsePreview('no markup at all'), { title: '', description: '', site: '' });
});
test('parsePreview decodes numeric entities and caps lengths', () => {
assert.equal(parsePreview(page('<title>caf&#233; &#x1F600; &unknown;</title>')).title, 'café 😀 &unknown;');
const long = parsePreview(page(`<meta name="description" content="${'x'.repeat(1000)}">`)).description;
assert.equal(long.length, 400);
assert.ok(long.endsWith('…'));
});
test('only public http(s) addresses are allowed', async () => {
const ok = async (u) => !!(await allowed(u, PUBLIC));
assert.equal(await ok('https://example.com/a?b=1'), true);
for (const u of ['file:///etc/passwd', 'ftp://example.com', 'javascript:alert(1)', 'http://localhost:3000', 'http://app.localhost', 'http://127.0.0.1', 'http://10.1.2.3', 'http://192.168.0.1', 'http://172.20.0.1',
'http://169.254.169.254/latest', 'http://100.64.0.1', 'http://[::1]/', 'http://[fd00::1]/', 'http://[::ffff:127.0.0.1]/', 'http://[::ffff:192.168.1.5]/', 'http://[::127.0.0.1]/', 'http://[::ffff:10.0.0.1]/', 'http://2130706433/', 'http://0x7f.1/', 'http://user:pw@example.com', 'not a url']) {
assert.equal(await ok(u), false, u);
}
// a name that resolves to a private address
assert.equal(!!(await allowed('https://evil.example', async () => [{ address: '10.0.0.5' }])), false);
assert.equal(!!(await allowed('https://evil.example', async () => [{ address: '93.184.216.34' }, { address: '127.0.0.1' }])), false);
assert.equal(!!(await allowed('https://nx.example', async () => { throw new Error('ENOTFOUND'); })), false);
});
test('fetchPreview reads the page, follows redirects, and checks every hop', async () => {
const seen = [];
const fetch = async (url) => {
seen.push(url);
if (url === 'https://a.example/') return reply('', { status: 301, headers: { location: '/next' } });
if (url === 'https://a.example/next') return reply(page('<title>Landed</title><meta name="description" content="here">'));
return reply('', { status: 404 });
};
assert.deepEqual(await fetchPreview('https://a.example/', { fetch, lookup: PUBLIC }), { title: 'Landed', description: 'here', site: '' });
assert.deepEqual(seen, ['https://a.example/', 'https://a.example/next']);
// a public page that bounces to an address on the local network is not followed
const bounce = async (url) => (url === 'https://b.example/' ? reply('', { status: 302, headers: { location: 'http://192.168.1.1/admin' } }) : reply(page('<title>Router</title>')));
const calls = [];
assert.equal(await fetchPreview('https://b.example/', { fetch: async (u) => { calls.push(u); return bounce(u); }, lookup: PUBLIC }), null);
assert.deepEqual(calls, ['https://b.example/']);
});
test('fetchPreview gives null for non-HTML, errors, empty pages and loops', async () => {
const lookup = PUBLIC;
assert.equal(await fetchPreview('https://c.example/', { fetch: async () => reply('{"a":1}', { type: 'application/json' }), lookup }), null);
assert.equal(await fetchPreview('https://c.example/', { fetch: async () => { throw new Error('offline'); }, lookup }), null);
assert.equal(await fetchPreview('https://c.example/', { fetch: async () => reply(page('')), lookup }), null);
assert.equal(await fetchPreview('https://c.example/', { fetch: async () => reply('', { status: 302, headers: { location: '/' } }), lookup }), null);
assert.equal(await fetchPreview('http://127.0.0.1/', { fetch: async () => { throw new Error('must not be called'); }, lookup }), null);
});
test('fetchPreview honours the page encoding', async () => {
const latin = Buffer.from(page('<meta charset="iso-8859-1"><title>Café</title>'), 'latin1');
const r = await fetchPreview('https://d.example/', { fetch: async () => reply(latin, { type: 'text/html' }), lookup: PUBLIC });
assert.equal(r.title, 'Café');
});