/* * A SOURCES WATCHER READS FROM THE SERVER AND ONLY FROM THE SERVER. * * The first OSINT walk through the browser moved a Russian court's search page to the owner's phone, * and the page printed the owner's mobile IP and city. The browser exits through the tailnet (the * owner's device) and hands walled sessions to the device ring — both right for a shop, both wrong * here. These tests hold the line: no browser, no device, no tailnet in the pass; a refused page is * recorded as blocked and never retried elsewhere; only new things reach the feed. */ import { describe, it, expect, beforeEach } from 'vitest'; import { createRequire } from 'module'; import { readFileSync, mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; process.env.PROFILE_DIR = mkdtempSync(join(tmpdir(), 'gb-sources-')); const require = createRequire(import.meta.url); const sw = require('../src/sourceWatch'); const feed = require('../src/watcherFeed'); const src = (f) => readFileSync(fileURLToPath(new URL('../src/' + f, import.meta.url)), 'utf8'); const publicLookup = async () => [{ address: '93.184.216.34' }]; const res = (status, body, headers = {}) => ({ status, ok: status < 400, headers: { get: (k) => headers[k.toLowerCase()] ?? (k.toLowerCase() === 'content-type' ? 'text/html; charset=utf-8' : null) }, arrayBuffer: async () => new TextEncoder().encode(body).buffer, body: null, json: async () => JSON.parse(body), }); const tgPage = (posts) => '' + posts.map(([id, text]) => `
${text}
`).join('') + ''; const newsPage = (heads) => 'x' + heads.map(([href, t]) => `${t}`).join('') + 'Share this article on Facebook right now please'; let n = 0; const wid = () => 'sources-test-' + (++n); const noSleep = async () => {}; describe('reading a page', () => { it('takes one item per Telegram post, keyed by the post id', () => { const items = sw.extractTelegram(tgPage([['101', 'Под Черняховском разбился военный самолёт «Су-30»'], ['102', 'short']])); expect(items).toHaveLength(1); expect(items[0]).toMatchObject({ key: 'tg:klops_news/101', url: 'https://t.me/klops_news/101', published: '2026-11-05T10:00:00+00:00' }); expect(items[0].title).toContain('«Су-30»'); }); it('takes same-site headline links, drops menus, other sites and tracking parameters', () => { const items = sw.extractHeadlines(newsPage([ ['/2026/11/05/strzelanie-na-poligonie?utm_source=fb', 'Ćwiczenia wojsk przy granicy z Królewcem potrwają do piątku'], ['https://www.tko.pl/x', 'Bartoszyce: nowe utrudnienia na przejściu granicznym Bezledy dziś'], ]), 'https://tko.pl/'); expect(items.map((i) => i.url)).toEqual(['https://tko.pl/2026/11/05/strzelanie-na-poligonie', 'https://www.tko.pl/x']); }); it('calls a refusal a wall, and a long news page that mentions a captcha not one', () => { expect(sw.isWall(429, 'whatever')).toBe(true); expect(sw.isWall(200, 'Just a moment... checking your browser')).toBe(true); expect(sw.isWall(200, 'Проверка браузера, подождите')).toBe(true); expect(sw.isWall(200, 'news '.repeat(1000) + 'captcha')).toBe(false); }); it('decodes a windows-1251 page', () => { const buf = Buffer.from([0xcf, 0xee, 0xeb, 0xfc, 0xf8, 0xe0]); // "Польша" expect(sw.decode(buf, 'text/html; charset=windows-1251')).toBe('Польша'); }); }); describe('only public https is read', () => { it('refuses http, credentials, odd ports, internal names and private addresses', async () => { await expect(sw.assertPublicUrl('http://example.com', { lookup: publicLookup })).rejects.toThrow(/https/); await expect(sw.assertPublicUrl('https://u:p@example.com', { lookup: publicLookup })).rejects.toThrow(/credentials/); await expect(sw.assertPublicUrl('https://example.com:8443', { lookup: publicLookup })).rejects.toThrow(/port/); await expect(sw.assertPublicUrl('https://ghost-browser.pod.svc', { lookup: publicLookup })).rejects.toThrow(/internal/); await expect(sw.assertPublicUrl('https://10.0.0.5/')).rejects.toThrow(/private/); await expect(sw.assertPublicUrl('https://evil.example', { lookup: async () => [{ address: '169.254.169.254' }] })).rejects.toThrow(/private/); await expect(sw.assertPublicUrl('https://example.com', { lookup: publicLookup })).resolves.toBeTruthy(); expect(sw.isPrivateIp('::ffff:127.0.0.1')).toBe(true); expect(sw.isPrivateIp('100.101.1.1')).toBe(true); // a tailnet address is not public either }); it('checks every redirect hop, so a public page cannot bounce the read into the cluster', async () => { const fetchImpl = async (u) => (u.startsWith('https://example.com') ? res(302, '', { location: 'https://internal.example/' }) : res(200, 'x')); const lookup = async (h) => (h === 'internal.example' ? [{ address: '10.43.0.1' }] : [{ address: '93.184.216.34' }]); await expect(sw.fetchPage('https://example.com/', { fetchImpl, lookup })).rejects.toThrow(/private/); }); it('keeps only https sources in the config', () => { const s = sw.normalizeSources([{ id: 'klops', url: 'https://t.me/s/klops_news', lang: 'ru' }, { id: 'bad', url: 'http://x.ru' }, { url: 'javascript:alert(1)' }]); expect(s.map((x) => [x.id, x.kind])).toEqual([['klops', 'telegram']]); }); }); describe('a pass files only what is new', () => { let clock; beforeEach(() => { clock = Date.parse('2026-11-05T08:00:00Z'); }); const cfg = { sources: [{ id: 'klops', name: 'Klops', url: 'https://t.me/s/klops_news', lang: 'ru' }], keywords: ['Польш', 'Черняхов'] }; const run = (id, page, opts = {}) => sw.pass(id, cfg, { feed, lookup: publicLookup, sleep: noSleep, now: () => clock, fetchImpl: async () => res(200, page), ...opts }); it('first pass is a baseline: it remembers everything and files only keyword hits', async () => { const id = wid(); const r = await run(id, tgPage([['1', 'Самолёт разбился под Черняховском во время учений'], ['2', 'Погода в Калининграде на выходные: дожди']])); expect(r.ran).toBe(true); const items = feed.list(id); expect(items).toHaveLength(1); expect(items[0]).toMatchObject({ source: 'Klops', sourceId: 'klops', lang: 'ru', keywordHits: ['Черняхов'], url: 'https://t.me/klops_news/1' }); expect(items[0].urgency).toBeGreaterThan(1); }); it('a later pass files the new posts only — a repeat is never filed twice', async () => { const id = wid(); await run(id, tgPage([['1', 'Погода в Калининграде на выходные: дожди']])); clock += sw.MIN_INTERVAL_MS; await run(id, tgPage([['1', 'Погода в Калининграде на выходные: дожди'], ['2', 'Новые ограничения на границе с Польшей с понедельника']])); clock += sw.MIN_INTERVAL_MS; await run(id, tgPage([['1', 'Погода в Калининграде на выходные: дожди'], ['2', 'Новые ограничения на границе с Польшей с понедельника']])); const items = feed.list(id); expect(items.map((i) => i.url)).toEqual(['https://t.me/klops_news/2']); expect(items[0].keywordHits).toEqual(['Польш']); }); it('a page that refuses the server is BLOCKED, files nothing and is not retried anywhere else', async () => { const id = wid(); const asked = []; const r = await run(id, '', { fetchImpl: async (u) => { asked.push(u); return res(429, 'Too many requests'); } }); expect(r.sources[0]).toMatchObject({ status: 'blocked', filed: 0 }); expect(asked).toEqual(['https://t.me/s/klops_news']); expect(feed.list(id)).toHaveLength(0); expect(sw.statusOf(id, cfg).sources[0].status).toBe('blocked'); }); it('reads at most once an hour on schedule, and not again within five minutes by hand', async () => { const id = wid(); const page = tgPage([['1', 'Погода в Калининграде на выходные: дожди']]); expect((await run(id, page)).ran).toBe(true); clock += 10 * 60e3; expect((await run(id, page)).ran).toBe(false); expect((await run(id, page, { force: true })).ran).toBe(true); clock += 60e3; expect((await run(id, page, { force: true })).ran).toBe(false); }); it('asks one page at a time, with a pause between requests', async () => { const id = wid(); let open = 0, most = 0; const pauses = []; const two = { ...cfg, sources: [...cfg.sources, { id: 'b', url: 'https://t.me/s/borderRussiaEuropa' }] }; await sw.pass(id, two, { feed, lookup: publicLookup, now: () => clock, sleep: async (ms) => { pauses.push(ms); }, fetchImpl: async () => { open++; most = Math.max(most, open); await new Promise((r) => setTimeout(r, 5)); open--; return res(200, tgPage([])); } }); expect(most).toBe(1); expect(pauses).toEqual([2000]); }); }); describe('feeds, replies and what gets filed', () => { let clock = Date.parse('2026-11-05T08:00:00Z'); const rss = (items, host = 'news.err.ee') => 'ERR' + items.map(([id, t, d]) => `<![CDATA[${t}]]>https://${host}/${id}?utm_source=rss${d || ''}Wed, 04 Nov 2026 10:00:00 GMT`).join('') + ''; it('reads an RSS feed: title, clean link, unescaped summary, ISO date', () => { const [it] = sw.extract({ kind: 'page', url: 'https://news.err.ee/rss' }, rss([['1', 'Drone spotted over Narva', '<p>An object crossed the &quot;border&quot;</p>']])); expect(it).toMatchObject({ url: 'https://news.err.ee/1', title: 'Drone spotted over Narva', published: '2026-11-04T10:00:00.000Z' }); expect(it.summary).toBe('An object crossed the "border"'); }); it('reads an Atom feed', () => { const atom = 'Border guard notice2026-11-04T09:00:00ZText'; expect(sw.extract({ kind: 'page', url: 'https://raja.fi/' }, atom)[0]).toMatchObject({ url: 'https://raja.fi/en/n/1', title: 'Border guard notice' }); }); it('a Telegram reply is the reply, not the message it quotes', () => { const html = '
The quoted parent message text here
' + '
The reply itself, about the border guard committee
'; expect(sw.extractTelegram(html)[0].title).toBe('The reply itself, about the border guard committee'); }); it('a general newsroom files keyword hits only; a source marked "all" files everything new', async () => { const id = wid(); const cfg2 = { keywords: ['Narva'], sources: [{ id: 'err', url: 'https://news.err.ee/rss' }, { id: 'ppa', url: 'https://www.politsei.ee/rss', all: true }] }; const pages = { first: rss([['0', 'Old news nobody needs']]), second: rss([['0', 'Old news nobody needs'], ['1', 'Football results tonight'], ['2', 'Drone over Narva river']]) }; let page = pages.first; const go = () => sw.pass(id, cfg2, { feed, lookup: publicLookup, sleep: noSleep, now: () => clock, fetchImpl: async (u) => res(200, u.includes('politsei') ? page.replace(/news\.err\.ee/g, 'www.politsei.ee') : page, { 'content-type': 'application/rss+xml' }) }); await go(); page = pages.second; clock += sw.MIN_INTERVAL_MS; await go(); const bySrc = (s) => feed.list(id).filter((i) => i.sourceId === s).map((i) => i.title).sort(); expect(bySrc('err')).toEqual(['Drone over Narva river']); expect(bySrc('ppa')).toEqual(['Drone over Narva river', 'Football results tonight']); }); it('a switched-off source stays in the list and is not read', async () => { const id = wid(); const asked = []; const cfg3 = { keywords: [], sources: [{ id: 'a', url: 'https://a.example/' }, { id: 'b', url: 'https://b.example/', enabled: false }] }; await sw.pass(id, cfg3, { feed, lookup: publicLookup, sleep: noSleep, now: () => clock, fetchImpl: async (u) => { asked.push(u); return res(200, ''); } }); expect(asked).toEqual(['https://a.example/']); expect(sw.statusOf(id, cfg3).sources.map((s) => [s.id, s.status])).toEqual([['a', 'thin'], ['b', 'off']]); }); }); describe('the address the sources see', () => { it('is measured with the same plain fetch and always reports via cluster', async () => { const out = await sw.egress({ fetchImpl: async () => res(200, JSON.stringify({ ip: '65.108.13.228', country: 'FI', org: 'AS24940 Hetzner Online GmbH' })) }); expect(out).toEqual({ ip: '65.108.13.228', country: 'FI', org: 'AS24940 Hetzner Online GmbH', via: 'cluster' }); }); }); describe('the pass cannot reach the owner', () => { it('sourceWatch.js uses no browser, no device ring, no tailnet', () => { const code = src('sourceWatch.js').replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, ''); for (const m of ['./pool', './device-hub', './sessionRing', './tailscale', 'playwright', './profiles', './deviceThread']) expect(code).not.toContain(m); }); it('the scheduler starts a sources pass before the device ring and the browser gate are consulted', () => { const s = src('server.js'); const tick = s.slice(s.indexOf('async function scheduleTick()')); const at = tick.indexOf("=== 'sources') { startSourcePass(wf); continue; }"); expect(at).toBeGreaterThan(0); expect(at).toBeLessThan(tick.indexOf('sites.needsDevice(')); expect(at).toBeLessThan(tick.indexOf('runPassOnDevice(')); expect(at).toBeLessThan(tick.indexOf('runningWatchers.add(wf.id)')); }); it('a sources pass never takes the one-browser gate', () => { const s = src('server.js'); const at = s.indexOf('function startSourcePass('); const fn = s.slice(at, s.indexOf('\n}\n', at)); expect(fn).toContain('sourcePasses.add(wf.id)'); expect(fn).not.toContain('runningWatchers.add'); expect(fn).not.toContain('pool.'); }); });