OverviewFilesCommitsIssuesDocsReleasesPeers

pearbrowser-desktop / test / content-shield-proxy.test.js

227 lines · javascript ref:
import test from 'node:test'
import assert from 'node:assert/strict'
import { EventEmitter } from 'node:events'
import Module from 'node:module'
import nodeCrypto from 'node:crypto'
import { createRequire } from 'node:module'

// hyper-proxy.js is Bare-targeted (bare-http1/bare-crypto); these tests only
// exercise the shield chokepoints, so the anongpt-gate shim approach applies.
const origLoad = Module._load
Module._load = function (request, parent, isMain) {
  if (request === 'bare-crypto') return nodeCrypto
  if (request === 'bare-http1') return {}
  return origLoad.call(this, request, parent, isMain)
}
const { HyperProxy, escapeStyleText } = (await import('../backend/hyper-proxy.js')).default
Module._load = origLoad

const require = createRequire(import.meta.url)
const { ContentShield } = require('../backend/content-shield.cjs')

const DRIVE = 'a'.repeat(64)

function makeProxy (shield) {
  const fetches = []
  const proxy = new HyperProxy(async () => null, () => {})
  proxy._port = 9876
  proxy.issueApiToken = () => 'test-token'
  proxy._hybridFetch = async (driveKeyHex, filePath) => {
    fetches.push(`${driveKeyHex}:${filePath}`)
    return { content: Buffer.from('served-bytes'), contentType: 'text/plain', source: 'test' }
  }
  if (shield) proxy.setContentShield(shield)
  return { proxy, fetches }
}

function makeReq (method, path) {
  const req = new EventEmitter()
  req.method = method
  req.url = path
  req.headers = {}
  req.socket = { remoteAddress: '127.0.0.1' }
  return req
}

function makeRes () {
  return {
    statusCode: 200,
    headers: {},
    chunks: [],
    setHeader (name, value) { this.headers[name.toLowerCase()] = value },
    write (chunk) { if (chunk) this.chunks.push(Buffer.from(chunk)) },
    end (chunk) {
      if (chunk) this.chunks.push(Buffer.from(chunk))
      this.body = Buffer.concat(this.chunks).toString('utf8')
      this.ended = true
    }
  }
}

test('a blocked subresource is refused before any P2P/relay fetch', async () => {
  const shield = new ContentShield({ builtinList: false })
  shield.addList('test', '||doubleclick.net^')
  const { proxy, fetches } = makeProxy(shield)

  const res = makeRes()
  await proxy._handle(makeReq('GET', `/hyper/${DRIVE}/vendor/doubleclick.net/ad.js`), res)

  assert.equal(res.statusCode, 403)
  assert.equal(res.headers['x-pear-shield'], 'blocked')
  assert.match(res.body, /Blocked by PearBrowser Shield/)
  assert.deepEqual(fetches, [])
  assert.equal(shield.stats().blocked, 1)
})

test('an ordinary request passes the shield and reaches the fetch path', async () => {
  const shield = new ContentShield({ builtinList: false })
  shield.addList('test', '||doubleclick.net^')
  const { proxy, fetches } = makeProxy(shield)

  const res = makeRes()
  await proxy._handle(makeReq('GET', `/hyper/${DRIVE}/app.js`), res)

  assert.equal(res.statusCode, 200)
  assert.equal(res.body, 'served-bytes')
  assert.deepEqual(fetches, [`${DRIVE}:/app.js`])
})

test('a disabled shield restores pass-through behavior', async () => {
  const shield = new ContentShield({ builtinList: false })
  shield.addList('test', '||doubleclick.net^')
  shield.setEnabled(false)
  const { proxy, fetches } = makeProxy(shield)

  const res = makeRes()
  await proxy._handle(makeReq('GET', `/hyper/${DRIVE}/vendor/doubleclick.net/ad.js`), res)

  assert.equal(res.statusCode, 200)
  assert.equal(fetches.length, 1)
})

test('HTML injection carries the cosmetic style block only when enabled', async () => {
  const shield = new ContentShield({ builtinList: false })
  shield.addList('test', '##.ad-banner')
  const { proxy } = makeProxy(shield)

  const html = '<html><head></head><body>hi</body></html>'
  const injected = (await proxy._injectHtmlHead(Buffer.from(html), DRIVE, `/hyper/${DRIVE}/index.html`)).toString('utf8')
  assert.match(injected, /<style data-pear-shield>/)
  assert.match(injected, /\.ad-banner \{ display: none !important; \}/)

  shield.setEnabled(false)
  const clean = (await proxy._injectHtmlHead(Buffer.from(html), DRIVE, `/hyper/${DRIVE}/index.html`)).toString('utf8')
  assert.doesNotMatch(clean, /data-pear-shield/)
})

test('a proxy without a shield behaves exactly as before', async () => {
  const { proxy, fetches } = makeProxy(null)

  const res = makeRes()
  await proxy._handle(makeReq('GET', `/hyper/${DRIVE}/vendor/doubleclick.net/ad.js`), res)
  assert.equal(res.statusCode, 200)
  assert.equal(fetches.length, 1)

  const html = '<html><head></head><body>hi</body></html>'
  const injected = (await proxy._injectHtmlHead(Buffer.from(html), DRIVE, `/hyper/${DRIVE}/index.html`)).toString('utf8')
  assert.doesNotMatch(injected, /data-pear-shield/)
})

const DRIVE_B = 'b'.repeat(64)

test('allowlisted drive restores pass-through; other drives still block', async () => {
  const shield = new ContentShield({ builtinList: false })
  shield.addList('test', '||doubleclick.net^')
  shield.allowlistDrive(DRIVE)
  const { proxy, fetches } = makeProxy(shield)

  const allowRes = makeRes()
  await proxy._handle(makeReq('GET', `/hyper/${DRIVE}/vendor/doubleclick.net/ad.js`), allowRes)
  assert.equal(allowRes.statusCode, 200)
  assert.equal(allowRes.headers['x-pear-shield'], 'allowlisted')
  assert.equal(fetches.length, 1)

  // Same rule on a non-allowlisted drive still 403s before fetch
  const blockRes = makeRes()
  const before = fetches.length
  await proxy._handle(makeReq('GET', `/hyper/${DRIVE_B}/vendor/doubleclick.net/ad.js`), blockRes)
  assert.equal(blockRes.statusCode, 403)
  assert.equal(blockRes.headers['x-pear-shield'], 'blocked')
  assert.equal(fetches.length, before)
})

test('strict mode injects confining CSP meta; non-strict drives omit it', async () => {
  const shield = new ContentShield({ builtinList: false })
  shield.setStrictDrive(DRIVE, true)
  const { proxy } = makeProxy(shield)

  const html = '<html><head></head><body>hi</body></html>'
  const strictHtml = (await proxy._injectHtmlHead(Buffer.from(html), DRIVE, `/hyper/${DRIVE}/index.html`)).toString('utf8')
  assert.match(strictHtml, /data-pear-shield-strict="1"/)
  assert.match(strictHtml, /Content-Security-Policy/)
  assert.match(strictHtml, /default-src 'self'/)
  assert.match(strictHtml, /connect-src 'self'/)

  const openHtml = (await proxy._injectHtmlHead(Buffer.from(html), DRIVE_B, `/hyper/${DRIVE_B}/index.html`)).toString('utf8')
  assert.doesNotMatch(openHtml, /data-pear-shield-strict/)
})

test('scriptlets ride the hash-authorized inject path', async () => {
  const shield = new ContentShield({ builtinList: false })
  shield.addList('s', '##+js(set-constant, ads.enabled, false)')
  const { proxy } = makeProxy(shield)

  const html = '<html><head></head><body>hi</body></html>'
  const injected = (await proxy._injectHtmlHead(Buffer.from(html), DRIVE, `/hyper/${DRIVE}/index.html`)).toString('utf8')
  assert.match(injected, /data-pear-scriptlet="set-constant"/)
  assert.match(injected, /ads\.enabled/)
  // Body is pure JS inside a script tag (sha256ScriptBody path)
  assert.match(injected, /<script data-pear-scriptlet="set-constant">[\s\S]*?<\/script>/)

  shield.setEnabled(false)
  const clean = (await proxy._injectHtmlHead(Buffer.from(html), DRIVE, `/hyper/${DRIVE}/index.html`)).toString('utf8')
  assert.doesNotMatch(clean, /data-pear-scriptlet/)
})

test('plugin styles and scripts inject only when enabled', async () => {
  const shield = new ContentShield({ builtinList: false })
  shield.applyPluginContribution('plug', {
    styles: { matches: ['*'], css: '.plug-hide { display:none }' },
    scripts: { matches: ['*'], js: 'window.__plug=1' }
  }, ['pear.content.styles', 'pear.content.scripts'])
  const { proxy } = makeProxy(shield)

  const html = '<html><head></head><body>hi</body></html>'
  const on = (await proxy._injectHtmlHead(Buffer.from(html), DRIVE, `/hyper/${DRIVE}/index.html`)).toString('utf8')
  assert.match(on, /data-pear-plugin-style/)
  assert.match(on, /\.plug-hide/)
  assert.match(on, /data-pear-plugin="plug"/)
  assert.match(on, /window\.__plug=1/)

  shield.setPluginEnabled('plug', false)
  const off = (await proxy._injectHtmlHead(Buffer.from(html), DRIVE, `/hyper/${DRIVE}/index.html`)).toString('utf8')
  assert.doesNotMatch(off, /data-pear-plugin-style/)
  assert.doesNotMatch(off, /data-pear-plugin="plug"/)
})

test('filter and plugin CSS cannot close the browser-owned style element', async () => {
  const payload = '</style><script>window.__escaped=1</script><style>'
  assert.equal(escapeStyleText(payload).includes('</style>'), false)

  const shield = new ContentShield({ builtinList: false })
  shield.addList('malicious-list', `##${payload}`)
  assert.equal(shield.cosmeticCssFor('example.test'), '')

  shield.applyPluginContribution('style-only', {
    styles: { matches: ['*'], css: payload }
  }, ['pear.content.styles'])
  const { proxy } = makeProxy(shield)
  const html = '<html><head></head><body>hi</body></html>'
  const injected = (await proxy._injectHtmlHead(Buffer.from(html), DRIVE, `/hyper/${DRIVE}/index.html`)).toString('utf8')
  const style = injected.match(/<style data-pear-plugin-style>([\s\S]*?)<\/style>/)
  assert.ok(style)
  assert.equal(style[1].includes('</style>'), false)
  assert.equal(injected.includes('<script>window.__escaped=1</script>'), false)
  assert.match(style[1], /\\3c \/style>/)
})