365 lines
17 KiB
JavaScript
365 lines
17 KiB
JavaScript
//генерирует конкретные задачи из шаблона урока
|
||
//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{};
|
||
}
|
||
} |