Files

76 lines
7.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import {test,mock} from 'node:test';
import {createServer} from 'node:http';
import {createSocket} from 'node:dgram';
import {chromium} from 'playwright';
import {useTestDatabase} from './test-database.ts';
useTestDatabase('browser');
const launch=chromium.launch.bind(chromium);
mock.method(chromium,'launch',async(options:Parameters<typeof chromium.launch>[0])=>{
try{return await launch(options);}catch(error){console.error('Chromium launch diagnostic:',(error as Error).message);throw error;}
});
const original=await import('../lib/http.ts');
let fixture='',proxied:string[]=[],cookieSeen=false;
mock.module(new URL('../lib/http.ts',import.meta.url).href,{namedExports:{...original,requestPublic:async(url:string,options:{headers?:Record<string,string>})=>{
proxied.push(url);
if(url.endsWith('/go-book'))return {code:302,headers:{location:'/book/111431'},body:Buffer.alloc(0),type:'text/html'};
if(url.endsWith('/session')){cookieSeen=options.headers?.cookie?.includes('anonymous_session=fixture')??false;return {code:200,headers:{'content-type':'text/plain'},body:Buffer.from('ok'),type:'text/plain'};}
if(url.includes('/probe/'))return {code:200,headers:{'content-type':'application/javascript'},body:Buffer.from('window.loadedChunks=(window.loadedChunks||0)+1;'),type:'application/javascript'};
if(url.endsWith('/redirect'))return {code:302,headers:{location:target},body:Buffer.alloc(0),type:'text/html'};
if(!/^https:\/\/(e\.lanbook\.com|book\.ru)\//.test(url))throw new Error('Unexpected transport destination');
return {code:200,headers:{'content-type':'text/html; charset=utf-8','set-cookie':['anonymous_session=fixture; Path=/; Secure; SameSite=Lax']},body:Buffer.from(fixture),type:'text/html'};
}}});
const {checkBrowser}=await import('../lib/browser.ts');
let target='';
test('sandboxed Chromium blocks private HTTP, redirects, frames, popups, workers, WebSockets and WebRTC UDP',async()=>{
let httpHits=0,udpHits=0;
const server=createServer((_req,res)=>{httpHits++;res.end('private');}),udp=createSocket('udp4');udp.on('message',()=>udpHits++);
await new Promise<void>(resolve=>server.listen(0,'127.0.0.1',resolve));await new Promise<void>(resolve=>udp.bind(0,'127.0.0.1',resolve));
const address=server.address();assert.ok(address&&typeof address!=='string');target='http://127.0.0.1:'+address.port+'/private';
const udpAddress=udp.address();
fixture=`<!doctype html><html><body><h1>Book</h1><div id="message">Loading</div><script>
fetch(${JSON.stringify(target)}).catch(()=>{});
fetch('https://e.lanbook.com/session').catch(()=>{});
fetch('https://e.lanbook.com/redirect').catch(()=>{});
const iframe=document.createElement('iframe');iframe.src=${JSON.stringify(target)};document.body.appendChild(iframe);
window.open(${JSON.stringify(target)});
new Worker(URL.createObjectURL(new Blob(['fetch('+JSON.stringify(${JSON.stringify(target)})+').catch(()=>{})'])));
try{new WebSocket(${JSON.stringify(target.replace('http:','ws:'))})}catch{}
try{const rtc=new RTCPeerConnection({iceServers:[{urls:'stun:127.0.0.1:${udpAddress.port}'}]});rtc.createDataChannel('test');rtc.createOffer().then(offer=>rtc.setLocalDescription(offer));}catch{}
setTimeout(()=>document.getElementById('message').textContent='Для доступа к чтению книги необходимо авторизоваться.',2000);
</script></body></html>`;
try{
const check=await checkBrowser('https://e.lanbook.com/book/111431');
assert.equal(check.status,'auth',check.reason);assert.equal(httpHits,0);assert.equal(udpHits,0);assert.equal(cookieSeen,true,'The fresh anonymous cookie must survive the pinned transport');
assert.ok(proxied.every(url=>url.startsWith('https://e.lanbook.com/')));assert.ok(proxied.includes('https://e.lanbook.com/redirect'));
}finally{await new Promise<void>(resolve=>server.close(()=>resolve()));udp.close();}
});
test('a CSS-hidden login template does not make a known library book unavailable',async()=>{
fixture='<html><head><style>.reader-login-template {display:none}</style></head><body><h1>Book</h1><div class="reader-login-template">Для доступа к чтению книги необходимо авторизоваться.</div><p>Only the catalogue card has loaded.</p></body></html>';
const check=await checkBrowser('https://e.lanbook.com/book/2');assert.equal(check.status,'unknown',check.reason);assert.equal(check.code,200,'The browser must actually render the fixture');
});
test('a burst of essential chunks is queued instead of dropping requests over the parallelism cap',async()=>{
fixture='<html><body><h1>Book</h1><div id="message">Loading</div><script>for(let i=0;i<16;i++){let s=document.createElement("script");s.src="/probe/"+i+".js";document.head.appendChild(s)}setInterval(()=>{if(window.loadedChunks===16)document.getElementById("message").textContent="Для доступа к чтению книги необходимо авторизоваться."},50)</script></body></html>';
const check=await checkBrowser('https://e.lanbook.com/book/3');assert.equal(check.status,'auth',check.reason);
});
test('a public document redirect is explicitly revalidated and rendered at its final URL',async()=>{
fixture='<html><body><h1>Book</h1>Для доступа к чтению книги необходимо авторизоваться.</body></html>';
const check=await checkBrowser('https://e.lanbook.com/go-book');assert.equal(check.status,'auth',check.reason);assert.equal(check.finalUrl,'https://e.lanbook.com/book/111431');
});
test('BOOK.ru CSS pseudo-element tooltip proves that only a guest fragment is available',async()=>{
fixture='<html><head><style>a::after{content:none}a:hover::after{content:"В гостевом режиме доступны только 20 страниц книги.";display:block}</style></head><body><h1>Book</h1><a href="https://reader.new.book.ru/">Ознакомиться</a></body></html>';
const check=await checkBrowser('https://book.ru/book/940586');assert.equal(check.status,'restricted',check.reason);assert.match(check.reason,/до 20 страниц/);
});
test('BOOK.ru locked orange preview is unavailable even without a loaded tooltip',async()=>{
fixture='<html><body><h1>Book</h1><a class="book_mode_orange__fixture" href="https://reader.new.book.ru/"><span class="book_marker2__fixture">Lock</span>Ознакомиться</a></body></html>';
const check=await checkBrowser('https://book.ru/book/940586');assert.equal(check.status,'restricted',check.reason);assert.equal(check.evidence?.rule,'bookru-locked-preview');assert.doesNotMatch(check.reason,/20/);
fixture=fixture.replace('</body>','<a href="https://reader.new.book.ru/">Читать полностью</a></body>');
assert.equal((await checkBrowser('https://book.ru/book/940586')).status,'unknown','A full-reading control contradicts the locked-preview-only signal');
});
test('a hidden BOOK.ru preview cannot prove a restriction through its pseudo-element',async()=>{
fixture='<html><head><style>a::after{content:"В гостевом режиме доступны только 20 страниц книги.";display:block}</style></head><body><h1>Book</h1><div style="display:none"><a href="https://reader.new.book.ru/">Ознакомиться</a></div></body></html>';
const check=await checkBrowser('https://book.ru/book/940586');assert.equal(check.status,'unknown',check.reason);assert.equal(check.code,200);
});