feat: capture undergraduate pages with scoped evidence

This commit is contained in:
Codex
2026-10-14 23:40:15 +08:00
parent ebe70e3eb2
commit d101ffbf85
4 changed files with 274 additions and 0 deletions
@@ -0,0 +1,35 @@
const assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm'),path=require('node:path');
const source=fs.readFileSync(path.join(__dirname,'../../lib/features/school/undergraduate/page_probe.dart'),'utf8');
const base=source.match(/undergraduateProbeBase = r'''([\s\S]*?)''';/)[1];
const home='https://eams.uestc.edu.cn/eams/';
function input(name,value='503') {
return {value,cloneNode(){return {stored:'523',setAttribute(k,v){this.stored=v;},get outerHTML(){return `<input name="${name}" value="${this.stored}">`;}};}};
}
function doc(url=home,{password=false,timetable=false,exams=false,frames=[],big=false}={}) {
const table={get outerHTML(){if(big)return '汉'.repeat(800000);return '<table id="manualArrangeCourseTable">课程</table>';}};
const exam={querySelector(){return {children:['课程序号','课程名称','考试日期','考试安排','考试地点','座位号','考试情况','考试类型','其它说明'].map(textContent=>({textContent}))};},outerHTML:'<table>考试</table>'};
return {location:new URL(url),querySelector(s){
if(s==='input[type="password"]')return password?{}:null;
if(s==='#manualArrangeCourseTable')return timetable?table:null;
if(s==='#stdExamTableForm')return exams?{}:null;return null;
},querySelectorAll(s){
if(s==='iframe,frame')return frames;
if(s==='a[href]')return [{getAttribute(){return '/eams/logout.action';}}];
if(s==='table')return exams?[exam]:[];
if(s==='script')return [{textContent:'var unitCount = 12; activity = new TaskActivity("data");',outerHTML:'<script>var unitCount = 12;</script>'}];
if(s==='input[name="semester.id"],input[name="examType.id"],input[name="params"]')return [input('semester.id'),input('examType.id','1')];
return [];
}};
}
function probe(document,capture=true){return JSON.parse(vm.runInNewContext(base.replaceAll('__CAPTURE__',String(capture)),{URL,TextEncoder,document,location:document.location}));}
assert.equal(probe(doc(home,{timetable:true}),false).pages.length,0,'status never captures timetable');
assert.equal(probe(doc(home,{timetable:true}),false).authenticated,true);
const captured=probe(doc(home,{timetable:true}));
assert.equal(captured.pages.length,1);assert.match(captured.pages[0].html,/value="503"/);assert.doesNotMatch(captured.pages[0].html,/value="523"/);
assert.equal(probe(doc(home,{password:true,timetable:true})).pages.length,0);
assert.equal(probe(doc('https://idas.uestc.edu.cn/authserver/login',{timetable:true})).pages.length,0);
assert.equal(probe(doc(home,{frames:[{contentDocument:doc(home,{timetable:true})},{contentDocument:doc('https://other.test/eams/',{timetable:true})}]})).pages.length,1);
assert.equal(probe(doc(home,{timetable:true,big:true})).error,'capture_limit');
assert.equal(probe(doc(home,{frames:Array.from({length:17},()=>({contentDocument:doc(home,{timetable:true})}))})).error,'capture_limit');
assert.equal(probe(doc(home,{exams:true})).pages[0].kind,'exams');
console.log('Undergraduate probe: status-only, actual values, same-origin frames, passwords and aggregate limits passed.');
@@ -0,0 +1,96 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:hepan_everyday/features/school/undergraduate/navigation_policy.dart';
import 'package:hepan_everyday/features/school/undergraduate/page_probe.dart';
void main() {
final uri = Uri.parse('https://eams.uestc.edu.cn/eams/');
test(
'navigation and capture distinguish official data from authentication',
() {
for (final host in [
'eams.uestc.edu.cn',
'idas.uestc.edu.cn',
'eportal.uestc.edu.cn',
]) {
expect(
UndergraduateNavigationPolicy.canNavigate(
Uri.https(host, '/'),
isMainFrame: true,
),
isTrue,
);
}
for (final value in [
'https://eams.uestc.edu.cn.attacker.test/eams/',
'https://eams.uestc.edu.cn:444/eams/',
'https://user@eams.uestc.edu.cn/eams/',
'http://eams.uestc.edu.cn/eams/',
]) {
expect(
UndergraduateNavigationPolicy.canNavigate(
Uri.parse(value),
isMainFrame: true,
),
isFalse,
);
}
expect(UndergraduateNavigationPolicy.isDataPage(uri), isTrue);
expect(
UndergraduateNavigationPolicy.isDataPage(
Uri.parse('https://idas.uestc.edu.cn/authserver/login'),
),
isFalse,
);
expect(
UndergraduateNavigationPolicy.secureIdentityRedirect(
Uri.parse('http://idas.uestc.edu.cn/authserver/login?service=abc'),
).toString(),
'https://idas.uestc.edu.cn/authserver/login?service=abc',
);
},
);
test(
'page evidence rejects changed URLs cross origins and aggregate oversize',
() {
Map<String, dynamic> value() => {
'topUrl': uri.toString(),
'authenticated': true,
'pages': [
{'kind': 'timetable', 'url': uri.toString(), 'html': 'table'},
],
};
expect(
UndergraduatePageEvidence.decode(jsonEncode(value()), uri).pages,
hasLength(1),
);
expect(
() => UndergraduatePageEvidence.decode({
...value(),
'topUrl': 'https://other.test',
}, uri),
throwsFormatException,
);
final cross = value();
(cross['pages'] as List).single['url'] = 'https://idas.uestc.edu.cn/';
expect(
() => UndergraduatePageEvidence.decode(cross, uri),
throwsFormatException,
);
final large = value();
(large['pages'] as List).single['html'] = '汉' * 800000;
expect(
() => UndergraduatePageEvidence.decode(large, uri),
throwsFormatException,
);
expect(
() => UndergraduatePageEvidence.decode({
...value(),
'error': 'capture_limit',
}, uri),
throwsFormatException,
);
},
);
}