fix: preserve anonymous sessions and ignore hidden access templates
This commit is contained in:
1 parent
3abe7e26ec
commit
71bf5fd926
4 files changed
+19
-9
No files matched your search
+6
-2
@@ -11,6 +11,10 @@ export function result(url:string,status:Check['status'],reason:string,extra:Par
|
||||
return {url,status,reason,checkedAt:new Date().toISOString(),rulesVersion:RULES_VERSION,profile:'anonymous',...extra};
|
||||
}
|
||||
function evidence(url:string,text:string,rule:string){const normalized=text.replace(/\s+/g,' ').trim(),at=normalized.search(/публикация\s+изъята|для\s+(?:получения\s+)?(?:доступа|чтения|просмотра)|чтобы\s+получить\s+полный\s+доступ|sign in to|чтение\s+книги\s+недоступно|книга\s+недоступна/i);return {text:normalized.slice(Math.max(0,at),Math.max(0,at)+500),url:evidenceUrl(url),rule};}
|
||||
function removeHidden(document:ReturnType<typeof parseHTML>['document']){
|
||||
document.querySelectorAll('script,style,[hidden],[aria-hidden="true"],.hidden').forEach(n=>n.remove());
|
||||
document.querySelectorAll('[style]').forEach(n=>{if(/(?:^|;)\s*(?:display\s*:\s*none|visibility\s*:\s*(?:hidden|collapse))\s*(?:!important)?\s*(?:;|$)/i.test(n.getAttribute('style')??''))n.remove();});
|
||||
}
|
||||
export function accessMessage(url:string,text:string):Check|null{
|
||||
const normalized=text.replace(/\s+/g,' ').trim();
|
||||
const preview=/доступн[а-я]*\s+(?:только\s+|первые\s+)?(\d{1,4})\s+страниц/i.exec(normalized);
|
||||
@@ -26,7 +30,7 @@ export function accessMessage(url:string,text:string):Check|null{
|
||||
}
|
||||
export function purchaseOffer(url:string,html:string):Check|null{
|
||||
const {document}=parseHTML(html),host=new URL(url).hostname;
|
||||
document.querySelectorAll('script,style,[hidden],[aria-hidden="true"],.hidden,[style*="display: none"],[style*="display:none"]').forEach(n=>n.remove());
|
||||
removeHidden(document);
|
||||
if(host==='znanium.ru'||host.endsWith('.znanium.ru')){
|
||||
const id=new URL(url).searchParams.get('id');
|
||||
for(const item of document.querySelectorAll('.book-offers__item')){
|
||||
@@ -47,7 +51,7 @@ export function purchaseOffer(url:string,html:string):Check|null{
|
||||
export function classifyResponse(url:string,r:{code:number;url:string;body:string;type:string}):Check{
|
||||
const base={finalUrl:evidenceUrl(r.url),code:r.code};
|
||||
const {document}=parseHTML(/html|text\//i.test(r.type)?r.body:'');
|
||||
document.querySelectorAll('script,style,nav,footer,header,[hidden],[aria-hidden="true"],.hidden').forEach(n=>n.remove());
|
||||
removeHidden(document);document.querySelectorAll('nav,footer,header').forEach(n=>n.remove());
|
||||
const title=(document.querySelector('title')?.textContent??'').trim();
|
||||
const text=(document.querySelector('main,[role="main"]')??document.querySelector('body')??document.documentElement)?.textContent??'';
|
||||
const message=accessMessage(r.url,text);if(message?.status==='broken')return {...message,url,...base};
|
||||
|
||||
+6
-2
@@ -29,7 +29,7 @@ export async function checkBrowser(url:string,parentSignal?:AbortSignal):Promise
|
||||
if(controller.signal.aborted||limit||!browserRequestAllowed(target,root,request.method(),request.resourceType())){await route.abort();return;}
|
||||
if(++requests>100||totalBytes>32000000||active.size>=10)throw new Error('Превышен лимит ресурсов страницы');
|
||||
if(navigation){if(++navigations>8)throw new Error('Слишком много переходов');await waitForHost(new URL(target).hostname,controller.signal);}
|
||||
const headers:Record<string,string>={};for(const key of ['accept','accept-language','origin','referer','cookie','content-type']){const value=request.headers()[key];if(value)headers[key]=value;}
|
||||
const allHeaders=await request.allHeaders(),headers:Record<string,string>={};for(const key of ['accept','accept-language','origin','referer','cookie','content-type']){const value=allHeaders[key];if(value)headers[key]=value;}
|
||||
const response=await requestPublic(target,{signal:controller.signal,maxBytes:8000000,method:request.method() as 'GET'|'HEAD'|'OPTIONS',headers});
|
||||
totalBytes+=response.body.length;if(totalBytes>32000000)throw new Error('Превышен лимит ресурсов страницы');
|
||||
if([429,503].includes(response.code)){
|
||||
@@ -54,7 +54,11 @@ export async function checkBrowser(url:string,parentSignal?:AbortSignal):Promise
|
||||
const capture=async()=>{
|
||||
await page.locator('body').waitFor({state:'attached'});
|
||||
// Only rendered text is evidence; hidden login templates and site chrome cannot decide book access.
|
||||
const text=await page.locator('body').innerText();
|
||||
const text=await page.locator('body').evaluate(body=>{
|
||||
const walker=document.createTreeWalker(body,NodeFilter.SHOW_TEXT),parts:string[]=[];let node:Node|null,size=0;
|
||||
while((node=walker.nextNode())&&size<100000){const el=node.parentElement;if(!el||el.closest('header,footer,nav,script,style,[role="navigation"],[role="banner"],[role="contentinfo"]'))continue;const style=getComputedStyle(el);if(style.visibility==='hidden'||style.display==='none'||!el.getClientRects().length)continue;const text=node.textContent??'';parts.push(text);size+=text.length;}
|
||||
return parts.join(' ');
|
||||
});
|
||||
const offers=await page.locator('.book-offers__item,.app-article-access').evaluateAll(nodes=>nodes.filter(node=>{const style=getComputedStyle(node);return style.display!=='none'&&style.visibility!=='hidden'&&node.getClientRects().length>0;}).map(node=>node.outerHTML).join(''));
|
||||
return {text,offers,url:page.url(),title:await page.title()};
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ test('Springer requires an eBook purchase offer, not the paper edition price',()
|
||||
assert.equal(purchaseOffer(url,block.replace('Access this book','This is an open access book')),null);
|
||||
});
|
||||
test('hidden templates, general login, annotations and successful HTTP do not confirm access',()=>{
|
||||
for(const body of ['<h1>Book</h1>Аннотация','<header><input type="password"/>Для чтения авторизуйтесь</header><p>Book card</p>','<div hidden>Для чтения необходимо авторизоваться</div>','<div class="hidden">Для чтения необходимо авторизоваться</div>'])assert.equal(classify(body).status,'unknown');
|
||||
for(const body of ['<h1>Book</h1>Аннотация','<header><input type="password"/>Для чтения авторизуйтесь</header><p>Book card</p>','<div hidden>Для чтения необходимо авторизоваться</div>','<div class="hidden">Для чтения необходимо авторизоваться</div>','<div style="display:none">Для чтения необходимо авторизоваться</div>','<div style="visibility: hidden !important">Для чтения необходимо авторизоваться</div>'])assert.equal(classify(body).status,'unknown');
|
||||
});
|
||||
test('old rule versions and expired checks are invalidated independently of course content',()=>{
|
||||
const now=Date.now(),fresh=result(lan,'auth','Login');assert.equal(checkCurrent(fresh,now),true);
|
||||
|
||||
@@ -5,12 +5,13 @@ import {createSocket} from 'node:dgram';
|
||||
import {useTestDatabase} from './test-database.ts';
|
||||
useTestDatabase('browser');
|
||||
const original=await import('../lib/http.ts');
|
||||
let fixture='',proxied:string[]=[];
|
||||
mock.module(new URL('../lib/http.ts',import.meta.url).href,{namedExports:{...original,requestPublic:async(url:string)=>{
|
||||
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('/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.endsWith('/redirect'))return {code:302,headers:{location:target},body:Buffer.alloc(0),type:'text/html'};
|
||||
if(!url.startsWith('https://e.lanbook.com/'))throw new Error('Unexpected transport destination');
|
||||
return {code:200,headers:{'content-type':'text/html; charset=utf-8'},body:Buffer.from(fixture),type:'text/html'};
|
||||
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='';
|
||||
@@ -22,6 +23,7 @@ test('sandboxed Chromium blocks private HTTP, redirects, frames, popups, workers
|
||||
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)});
|
||||
@@ -32,7 +34,7 @@ test('sandboxed Chromium blocks private HTTP, redirects, frames, popups, workers
|
||||
</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(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();}
|
||||
});
|
||||
Reference in new issue
Block a user