Добавлено больше уроков, AI подсказки, генерация заданий, поиск и фильтрация уроков

This commit is contained in:
belred
2026-04-30 22:26:01 +03:00
parent 3c1c263c41
commit cc03166579
17 changed files with 2845 additions and 831 deletions
+365
View File
@@ -0,0 +1,365 @@
//генерирует конкретные задачи из шаблона урока
//AI не участвует — числа выбираются случайно в заданном диапазоне
function rnd(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
function randomUnicastMAC() {
const m = Array.from({ length: 6 }, () => rnd(0, 255));
m[0] = m[0] & 0xFE; // бит 0 = 0 (unicast)
if (m[0] === 0) m[0] = 0x02;
return m;
}
function randomMulticastMAC() {
const m = Array.from({ length: 6 }, () => rnd(0, 255));
m[0] = (m[0] | 0x01) & 0xFD;
if (m[0] === 0xFF) m[0] = 0x01;
return m;
}
function macStr(m) { return m.map(b => b.toString(16).padStart(2,'0').toUpperCase()).join(':'); }
function randomPublicIP() {
let ip;
do {
ip = [rnd(1,223), rnd(0,255), rnd(0,255), rnd(1,254)];
} while (
ip[0]===0 || ip[0]===10 || ip[0]===127 ||
(ip[0]===172 && ip[1]>=16 && ip[1]<=31) ||
(ip[0]===192 && ip[1]===168) ||
(ip[0]===169 && ip[1]===254) ||
(ip[0]===100 && ip[1]>=64 && ip[1]<=127) ||
ip[0]>=224
);
return ip;
}
function randomPrivateIP() {
const t = rnd(0,2);
if (t===0) return [10, rnd(0,255), rnd(0,255), rnd(1,254)];
if (t===1) return [172, rnd(16,31), rnd(0,255), rnd(1,254)];
return [192, 168, rnd(0,255), rnd(1,254)];
}
function randomEphemeralPort() { return rnd(49152, 65535); }
function randomHostname() {
const subs = ['api','cdn','app','mail','news','shop','auth','www','dev','data','files'];
const names = ['example','service','platform','network','cloud','tech','store','media','hub'];
const tlds = ['com','net','io','org','ru'];
return `${pick(subs)}.${pick(names)}.${pick(tlds)}`;
}
function randomPath() {
const a = ['users','products','orders','articles','posts','files','events','reports','tasks'];
const b = ['list','search','latest','popular','archive','create','delete'];
return Math.random()>0.5 ? '/'+pick(a) : '/'+pick(a)+'/'+pick(b);
}
function randomJSONBody() {
const bodies = [
{ name: pick(['Alice','Bob','Carol','Dave','Eve','Max']) },
{ user: pick(['admin','guest','student','teacher']), active: true },
{ title: pick(['Hello','Update','Report','Notice','Draft']) },
{ id: rnd(1,9999), status: pick(['active','pending','done']) },
{ email: `user${rnd(1,99)}@example.com` },
{ count: rnd(1,100), page: rnd(1,10) },
];
return JSON.stringify(pick(bodies));
}
export function generateTask(taskTemplate) {
switch (taskTemplate.type) {
case 'bit-set': {
const target = rnd(1, 255);
const hex = '0x' + target.toString(16).toUpperCase().padStart(2,'0');
return {
objective: `Установите биты так, чтобы получить число ${target} (${hex})`,
initialBuffer: new Uint8Array([0]),
validate: buf => buf[0] === target,
aiContext: { type:'bit-set', target, hex, binary: target.toString(2).padStart(8,'0'),
description: `Выставить биты байта = ${target} (${hex})` },
};
}
case 'mac-set': {
const v = rnd(0,2);
if (v===0) {
const m = randomUnicastMAC(), s = macStr(m);
return { objective:`Установите unicast MAC-адрес: ${s}`,
initialBuffer: new Uint8Array(6),
validate: buf => m.every((b,i) => buf[i]===b),
aiContext:{ type:'mac-set', mac:s, description:`unicast MAC ${s}` }};
}
if (v===1) return { objective:'Установите broadcast MAC-адрес',
initialBuffer: new Uint8Array(6),
validate: buf => Array.from(buf.slice(0,6)).every(b=>b===0xFF),
aiContext:{ type:'mac-set', mac:'FF:FF:FF:FF:FF:FF', description:'broadcast' }};
const m = randomMulticastMAC(), s = macStr(m);
return { objective:`Установите multicast MAC-адрес: ${s}`,
initialBuffer: new Uint8Array(6),
validate: buf => m.every((b,i) => buf[i]===b),
aiContext:{ type:'mac-set', mac:s, description:`multicast MAC ${s}` }};
}
case 'ethernet-frame': {
const etTypes = [
{ et:[0x08,0x00], name:'IPv4' },
{ et:[0x08,0x06], name:'ARP' },
{ et:[0x86,0xDD], name:'IPv6' },
];
const chosen = pick(etTypes);
const dv = rnd(0,2);
let dst, dstDesc;
if (dv===0) { dst=[0xFF,0xFF,0xFF,0xFF,0xFF,0xFF]; dstDesc='broadcast'; }
else if (dv===1) { dst=randomMulticastMAC(); dstDesc=macStr(dst)+' (multicast)'; }
else { dst=randomUnicastMAC(); dstDesc=macStr(dst)+' (unicast)'; }
const src=randomUnicastMAC();
const etHex='0x'+chosen.et.map(b=>b.toString(16).padStart(2,'0').toUpperCase()).join('');
return {
objective:`Соберите Ethernet-кадр: Dst MAC=${dstDesc}, Src MAC=${macStr(src)}, EtherType=${chosen.name}`,
initialBuffer: new Uint8Array(14+46+4),
validate: buf => buf.length>=14 && dst.every((b,i)=>buf[i]===b)
&& src.every((b,i)=>buf[6+i]===b) && buf[12]===chosen.et[0] && buf[13]===chosen.et[1],
aiContext:{ type:'ethernet-frame', dstMAC:macStr(dst), srcMAC:macStr(src),
etherType:etHex, protoName:chosen.name,
description:`dst=${macStr(dst)} src=${macStr(src)} et=${etHex}` },
};
}
case 'ipv4-addresses': {
const src=randomPrivateIP(), dst=randomPublicIP();
const ttl=pick([64,128,255]);
const p=pick([{val:6,name:'TCP'},{val:17,name:'UDP'},{val:1,name:'ICMP'}]);
return {
objective:`Соберите IPv4 заголовок: Src=${src.join('.')}, Dst=${dst.join('.')}, Protocol=${p.name}, TTL=${ttl}`,
initialBuffer: new Uint8Array([0x45,0x00,0x00,0x14,0,0,0x40,0,0,0,0,0,0,0,0,0,0,0,0,0]),
validate: buf => buf.length>=20 && buf[8]===ttl && buf[9]===p.val
&& src.every((b,i)=>buf[12+i]===b) && dst.every((b,i)=>buf[16+i]===b),
aiContext:{ type:'ipv4-addresses', srcIP:src.join('.'), dstIP:dst.join('.'),
ttl, protocol:p.val, protoName:p.name,
description:`src=${src.join('.')} dst=${dst.join('.')} ttl=${ttl} proto=${p.name}` },
};
}
case 'ipv4-ttl': {
const ttl = Math.random()>0.4 ? pick([64,128,255,32]) : rnd(1,254);
const label={64:' (Linux/macOS)',128:' (Windows)',255:' (сетевые устройства)'}[ttl]??'';
const src=randomPrivateIP(), dst=randomPublicIP();
return {
objective:`Установите TTL = ${ttl}${label}`,
initialBuffer: new Uint8Array([0x45,0x00,0x00,0x14,0xAB,0xCD,0x40,0x00,
0x00,0x06,0x00,0x00,...src,...dst]),
validate: buf => buf[8]===ttl,
aiContext:{ type:'ipv4-ttl', ttl, hex:'0x'+ttl.toString(16).toUpperCase().padStart(2,'0'),
description:`TTL=${ttl} байт 8` },
};
}
case 'ipv4-fragmentation': {
const scenarios=[
{b:[0x20,0x00], desc:'первый фрагмент: MF=1, DF=0, Offset=0'},
{b:[0x40,0x00], desc:"Don't Fragment (DF=1): фрагментация запрещена, MF=0, Offset=0"},
{b:[0x00,0xB9], desc:'последний фрагмент: MF=0, DF=0, Offset=185 (1480 байт ÷ 8)'},
{b:[0x20,0xB9], desc:'промежуточный фрагмент: MF=1, DF=0, Offset=185'},
{b:[0x00,0x2E], desc:'фрагмент с Offset=46 (368 байт ÷ 8): MF=0, DF=0'},
];
const s=pick(scenarios);
const src=randomPrivateIP(), dst=randomPublicIP();
return {
objective: 'Установите ' + s.desc,
initialBuffer:new Uint8Array([0x45,0x00,0x00,0x14,rnd(0,255),rnd(0,255),
0x40,0x00,0x40,0x11,0x00,0x00,...src,...dst]),
validate:buf=>buf[6]===s.b[0]&&buf[7]===s.b[1],
aiContext:{type:'ipv4-fragmentation',b6:s.b[0],b7:s.b[1],description:s.desc},
};
}
case 'tcp-header': {
const svcs=[{p:80,n:'HTTP'},{p:443,n:'HTTPS'},{p:22,n:'SSH'},
{p:25,n:'SMTP'},{p:3306,n:'MySQL'},{p:5432,n:'PostgreSQL'}];
const svc=pick(svcs), sp=randomEphemeralPort(), seq=rnd(0,0xFFFFFF);
const win=pick([8192,16384,32768,65535]);
return {
objective:`Соберите TCP SYN к ${svc.n}: Src Port=${sp}, Dst Port=${svc.p}, Seq=${seq}, SYN (0x02), Window=${win}`,
initialBuffer:new Uint8Array(20).fill(0).map((_,i)=>i===12?0x50:0),
validate:buf=>buf.length>=20
&&((buf[0]<<8)|buf[1])===sp&&((buf[2]<<8)|buf[3])===svc.p
&&(((buf[4]<<24)|(buf[5]<<16)|(buf[6]<<8)|buf[7])>>>0)===seq
&&buf[13]===0x02&&((buf[14]<<8)|buf[15])===win,
aiContext:{type:'tcp-header',srcPort:sp,dstPort:svc.p,seq,flags:0x02,window:win,service:svc.n,
description:`TCP SYN sp=${sp} dp=${svc.p} seq=${seq}`},
};
}
case 'tcp-flags': {
const combos=[
{f:0x02,n:'SYN', d:'инициация соединения (1-й шаг handshake)'},
{f:0x10,n:'ACK', d:'подтверждение получения данных'},
{f:0x12,n:'SYN+ACK', d:'ответ сервера (2-й шаг handshake)'},
{f:0x01,n:'FIN', d:'инициация завершения соединения'},
{f:0x11,n:'FIN+ACK', d:'завершение с подтверждением'},
{f:0x04,n:'RST', d:'немедленный сброс соединения'},
{f:0x18,n:'PSH+ACK', d:'передача данных без буферизации'},
];
const c=pick(combos), sp=randomEphemeralPort(), dp=pick([80,443,22,25,3306]);
const seq=rnd(0,0xFFFFFF);
return {
objective:`Установите TCP-флаги: ${c.n}${c.d}. Байт флагов = 0x${c.f.toString(16).toUpperCase().padStart(2,'0')}`,
initialBuffer:new Uint8Array([(sp>>8)&0xFF,sp&0xFF,(dp>>8)&0xFF,dp&0xFF,
(seq>>24)&0xFF,(seq>>16)&0xFF,(seq>>8)&0xFF,seq&0xFF,0,0,0,0,
0x50,0x00,0xFF,0xFF,0,0,0,0]),
validate:buf=>buf[13]===c.f,
aiContext:{type:'tcp-flags',flagName:c.n,
flagHex:'0x'+c.f.toString(16).toUpperCase().padStart(2,'0'),
description:`TCP флаги ${c.n} байт 13`},
};
}
case 'udp-ports': {
const svcs=[{d:53,n:'DNS'},{d:123,n:'NTP'},{d:67,n:'DHCP'},
{d:161,n:'SNMP'},{d:514,n:'Syslog'},{d:5353,n:'mDNS'},
{d:1194,n:'OpenVPN'},{d:4500,n:'IPSec NAT-T'}];
const svc=pick(svcs), sp=randomEphemeralPort();
return {
objective:`Соберите UDP для ${svc.n}: Src Port=${sp}, Dst Port=${svc.d}`,
initialBuffer:new Uint8Array(8),
validate:buf=>((buf[0]<<8)|buf[1])===sp&&((buf[2]<<8)|buf[3])===svc.d,
aiContext:{type:'udp-ports',srcPort:sp,dstPort:svc.d,service:svc.n,
description:`UDP src=${sp} dst=${svc.d} (${svc.n})`},
};
}
case 'http-get': {
const host=randomHostname(), path=randomPath();
const accept=pick(['application/json','text/html','application/xml','text/plain']);
return {
objective:`Отправьте GET-запрос: ресурс ${path} с сервера ${host}, Accept: ${accept}`,
initialBuffer:new TextEncoder().encode('GET / HTTP/1.1\n'),
validate:buf=>{
const txt=new TextDecoder().decode(buf).replace(/\r\n/g,'\n');
const lines=txt.split('\n');
const rl=lines[0]?.match(/^(\S+)\s+(\S+)\s+(\S+)$/);
if(!rl||rl[1]!=='GET'||rl[2]!==path||rl[3]!=='HTTP/1.1') return false;
const h={};
for(let i=1;i<lines.length;i++){
if(!lines[i].trim()) break;
const c=lines[i].indexOf(':');
if(c>0) h[lines[i].slice(0,c).trim().toLowerCase()]=lines[i].slice(c+1).trim();
}
return h['host']===host&&(h['accept']??'').includes(accept)&&txt.includes('\n\n');
},
aiContext:{type:'http-get',host,path,accept,description:`GET ${path} от ${host}`},
};
}
case 'http-post': {
const host=randomHostname(), path=randomPath();
const body=randomJSONBody(), bl=new TextEncoder().encode(body).length;
return {
objective:`Отправьте POST на ${host}${path}: тело ${body}, Content-Type: application/json`,
initialBuffer:new TextEncoder().encode('POST / HTTP/1.1\n'),
validate:buf=>{
const txt=new TextDecoder().decode(buf).replace(/\r\n/g,'\n');
const lines=txt.split('\n');
const rl=lines[0]?.match(/^(\S+)\s+(\S+)\s+(\S+)$/);
if(!rl||rl[1]!=='POST'||rl[2]!==path||rl[3]!=='HTTP/1.1') return false;
const h={};let bs=lines.length;
for(let i=1;i<lines.length;i++){
if(!lines[i].trim()){bs=i+1;break;}
const c=lines[i].indexOf(':');
if(c>0) h[lines[i].slice(0,c).trim().toLowerCase()]=lines[i].slice(c+1).trim();
}
if(h['host']!==host) return false;
if(!(h['content-type']??'').includes('application/json')) return false;
const bodyText=lines.slice(bs).join('\n').trim();
try{ if(JSON.stringify(JSON.parse(bodyText))!==JSON.stringify(JSON.parse(body))) return false; }
catch{ return false; }
return parseInt(h['content-length']??'',10)===new TextEncoder().encode(bodyText).length
&&txt.includes('\n\n');
},
aiContext:{type:'http-post',host,path,body,bodyLength:bl,
description:`POST ${path}${host}`},
};
}
case 'dns-header': {
const txId=rnd(1,0xFFFE);
const wantRD=Math.random()>0.4;
const flags=wantRD?0x0100:0x0000;
const fdesc=wantRD?'рекурсивный запрос (RD=1)':'итеративный запрос (RD=0)';
return {
objective:`Отправьте DNS-запрос: ID=${txId}, ${fdesc}, QDCOUNT=1`,
initialBuffer:new Uint8Array(12),
validate:buf=>buf.length>=6
&&((buf[0]<<8)|buf[1])===txId
&&((buf[2]<<8)|buf[3])===flags
&&((buf[4]<<8)|buf[5])===1,
aiContext:{type:'dns-header',txId,flags,flagsDesc:fdesc,
txH:(txId>>8)&0xFF,txL:txId&0xFF,
description:`DNS ID=${txId} Flags=0x${flags.toString(16).padStart(4,'0')}`},
};
}
case 'dns-query': {
const qtypes=[
{val:1, n:'A', d:'IPv4-адрес'},
{val:28,n:'AAAA', d:'IPv6-адрес'},
{val:15,n:'MX', d:'почтовый сервер'},
{val:2, n:'NS', d:'DNS-сервер зоны'},
{val:5, n:'CNAME', d:'псевдоним'},
{val:16,n:'TXT', d:'текстовая запись'},
];
const qt=pick(qtypes), txId=rnd(1,0xFFFE);
return {
objective:`Отправьте запрос на example.com: QTYPE=${qt.n}, QCLASS=IN(1)`,
initialBuffer:new Uint8Array([
(txId>>8)&0xFF,txId&0xFF,0x01,0x00,0x00,0x01,0,0,0,0,0,0,
0x07,0x65,0x78,0x61,0x6D,0x70,0x6C,0x65,
0x03,0x63,0x6F,0x6D,0x00,
0x00,0x00,0x00,0x00,
]),
validate:buf=>buf.length>=29&&((buf[25]<<8)|buf[26])===qt.val&&((buf[27]<<8)|buf[28])===1,
aiContext:{type:'dns-query',qtypeName:qt.n,qtypeVal:qt.val,
description:`QTYPE=${qt.n}(${qt.val}), QCLASS=IN`},
};
}
default:
throw new Error(`Unknown task type: ${template.type}`);
}
}
export function buildCurrentState(buf, ctx) {
if (!buf||!ctx) return {};
switch(ctx.type){
case 'bit-set':
return{value:buf[0],binary:buf[0].toString(2).padStart(8,'0'),
hex:'0x'+buf[0].toString(16).toUpperCase().padStart(2,'0')};
case 'mac-set': case 'ethernet-frame':
return{dst:Array.from(buf.slice(0,6)).map(b=>b.toString(16).padStart(2,'0').toUpperCase()).join(':'),
src:Array.from(buf.slice(6,12)).map(b=>b.toString(16).padStart(2,'0').toUpperCase()).join(':')};
case 'ipv4-addresses': case 'ipv4-fragmentation':
return{srcIP:Array.from(buf.slice(12,16)).join('.'),dstIP:Array.from(buf.slice(16,20)).join('.'),
ttl:buf[8],proto:buf[9],
b6:'0x'+(buf[6]??0).toString(16).padStart(2,'0').toUpperCase(),
b7:'0x'+(buf[7]??0).toString(16).padStart(2,'0').toUpperCase()};
case 'ipv4-ttl':
return{ttl:buf[8],hex:'0x'+buf[8].toString(16).toUpperCase().padStart(2,'0')};
case 'tcp-header':
return{srcPort:(buf[0]<<8)|buf[1],dstPort:(buf[2]<<8)|buf[3],
seq:((buf[4]<<24)|(buf[5]<<16)|(buf[6]<<8)|buf[7])>>>0,
flags:'0x'+buf[13].toString(16).padStart(2,'0').toUpperCase(),
window:(buf[14]<<8)|buf[15]};
case 'tcp-flags':
return{flags:buf[13],hex:'0x'+buf[13].toString(16).toUpperCase().padStart(2,'0')};
case 'udp-ports':
return{srcPort:(buf[0]<<8)|buf[1],dstPort:(buf[2]<<8)|buf[3]};
case 'http-get': case 'http-post':
return{text:new TextDecoder().decode(buf).slice(0,300)};
case 'dns-header':
return{id:(buf[0]<<8)|buf[1],
flags:'0x'+((buf[2]<<8)|buf[3]).toString(16).padStart(4,'0').toUpperCase(),
qdcount:(buf[4]<<8)|buf[5]};
case 'dns-query':
return{qtype:(buf[25]<<8)|buf[26],qclass:(buf[27]<<8)|buf[28]};
default: return{};
}
}