'use strict'; const assert=require('assert/strict'); const fs=require('fs'); const fsp=fs.promises; const http=require('http'); const os=require('os'); const path=require('path'); const {spawn}=require('child_process'); const {root,starterPuzzle}=require('./helpers/app-source'); const {createRealtimeHub}=require('../realtime-server'); const {checkPublicDeployment}=require('../server/public-health'); const BuildMeta=require('../build-meta'); const service=require('../scripts/service-control'); const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms)); function auth(session){return{authorization:`Bearer ${session.playerId}.${session.token}`,'content-type':'application/json'}} function boardMeta(id,x,seed,puzzle){return{id,x,y:0,chunks:[[0,0]],level:1,targetLevel:1,seed,axis:'MIX',sealedSides:[],puzzle,rev:1,revAuthor:'client',untrustedBulk:{ignored:true}}} (async()=>{ const unitServer=http.createServer(),hub=createRealtimeHub({server:unitServer,authenticate:async()=>null,getBoardInfo:async()=>null,maxClients:10,maxClientsPerPlayer:5}); try{const identity={playerId:'a'.repeat(24),name:'A'},client=hub.createPollingClient(identity);for(let i=0;i<300;i++)hub.broadcastClearEvents([{id:`B${i}`}]);const envelope=hub.pollPollingClient(identity,client.presenceId,0);assert.equal(envelope.eventGap,true);assert(envelope.messages.length<=256);let live=envelope;for(let i=0;i<=300&&live;i++)live=await hub.handlePollingMessage(identity,client.presenceId,{type:'snapshot-request'},live.sequence);assert.equal(live,null,'Realtime message flood was not disconnected')}finally{hub.close()} let discloseVersion=false;const healthServer=http.createServer((req,res)=>{res.setHeader('server',discloseVersion?'Apache/2.4.58':'Apache');if(req.url==='/'){res.writeHead(200,{'content-type':'text/html'});return res.end('LinkField')}if(['/server.js','/package.json','/scripts/service-control.js','/.linkfield-deployment.json'].includes(req.url)){res.writeHead(403);return res.end()}const body=JSON.stringify({available:true,appVersion:BuildMeta.APP_VERSION});if(req.url?.startsWith('/api-bridge.php'))res.setHeader('x-linkfield-bridge','php');res.writeHead(200,{'content-type':'application/json'});res.end(body)});await new Promise(resolve=>healthServer.listen(0,'127.0.0.1',resolve));const healthBase=`http://127.0.0.1:${healthServer.address().port}/`;try{const checked=await checkPublicDeployment(healthBase,{appVersion:BuildMeta.APP_VERSION});assert.equal(checked.base,healthBase);discloseVersion=true;await assert.rejects(()=>checkPublicDeployment(healthBase,{appVersion:BuildMeta.APP_VERSION}),/discloses a detailed version/)}finally{await new Promise(resolve=>healthServer.close(resolve))} const dataRoot=await fsp.mkdtemp(path.join(os.tmpdir(),'linkfield-hardening-')),publicRoot=path.join(dataRoot,'public'),port=36000+Math.floor(Math.random()*2000),base=`http://127.0.0.1:${port}`; await service.deployPublicFiles(publicRoot); let symlinkCreated=false;try{await fsp.symlink(path.join(root,'server.js'),path.join(publicRoot,'assets','private-link.js'),'file');symlinkCreated=true}catch(error){if(!['EPERM','EACCES','ENOTSUP'].includes(error?.code))throw error} const child=spawn(process.execPath,[path.join(root,'server.js')],{env:{...process.env,HOST:'127.0.0.1',PORT:String(port),LINK_FIELD_TEST_DATA_ROOT:dataRoot,LINK_FIELD_PUBLIC_DIR:publicRoot},stdio:['ignore','pipe','pipe']});let stderr='';child.stderr.on('data',chunk=>stderr+=chunk); async function request(url,options={}){const response=await fetch(base+url,options),text=await response.text();let body;try{body=JSON.parse(text)}catch{body={raw:text}}return{response,body}} async function stop(){child.kill('SIGTERM');await Promise.race([new Promise(resolve=>child.once('exit',resolve)),sleep(3000)]);if(child.exitCode==null)child.kill('SIGKILL')} try{ for(let i=0;i<100;i++){try{if((await request('/api/cloud/status')).response.ok)break}catch(_){}await sleep(30)} const privateFile=await request('/package.json');assert.equal(privateFile.response.status,404);if(symlinkCreated)assert.equal((await request('/assets/private-link.js')).response.status,404);const publicScript=await fetch(`${base}/app.js`);assert.equal(publicScript.status,200);assert.match(publicScript.headers.get('cache-control')||'',/max-age=300/); const alice=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:'{"name":"Alice"}'})).body,bob=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:'{"name":"Bob"}'})).body; const puzzle=starterPuzzle(),b0=boardMeta('B0',0,11,puzzle),b1=boardMeta('B1',1,12,puzzle); const initialPush={baseRevision:0,mutationId:'alice-init-0001',global:{worldGeneration:'attacker',schema:-1,appVersion:'fake',quarantine:{blob:'x'.repeat(1000)}},metas:[b0,b1],states:[{id:'B0',value:{paths:[],solved:false}},{id:'B1',value:{paths:[],solved:false}}]};let result=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify(initialPush)});assert.equal(result.response.status,200,JSON.stringify(result.body));const duplicate=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify(initialPush)});assert.equal(duplicate.response.status,200);assert.equal(duplicate.body.duplicate,true);assert.equal(duplicate.body.revision,1); result=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:1,mutationId:'alice-expand-0002',metas:[{id:'B2'}],states:[]})});assert.equal(result.response.status,403,'Expansion authorization must run before expensive metadata validation'); const aliceConnect=await request('/api/realtime/connect',{method:'POST',headers:auth(alice),body:'{}'}),bobConnect=await request('/api/realtime/connect',{method:'POST',headers:auth(bob),body:'{}'});assert.equal(aliceConnect.response.status,200);assert.equal(bobConnect.response.status,200); for(const[session,connected,id]of[[alice,aliceConnect.body,'B0'],[bob,bobConnect.body,'B1']]){const claim=await request('/api/realtime/claim',{method:'POST',headers:auth(session),body:JSON.stringify({presenceId:connected.presenceId,boardId:id})});assert.equal(claim.body.ok,true)} result=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:1,mutationId:'alice-state-0003',global:{worldGeneration:'poisoned'},metas:[],states:[{id:'B0',value:{paths:[],specialProgress:{crossings:[]},solved:false,unknown:{blob:'x'.repeat(1000)}}}]})});assert.equal(result.response.status,200); result=await request('/api/cloud/push',{method:'POST',headers:auth(bob),body:JSON.stringify({baseRevision:1,mutationId:'bob-state-0001',global:{},metas:[],states:[{id:'B1',value:{paths:[],solved:false}}]})});assert.equal(result.response.status,200);assert.equal(result.body.rebased,true); const pull=await request('/api/cloud/pull?since=0',{headers:auth(alice)});assert.equal(pull.body.page.global.worldGeneration,BuildMeta.WORLD_GENERATION);assert.equal(pull.body.page.global.appVersion,BuildMeta.APP_VERSION);assert.equal(pull.body.page.global.quarantine,undefined);assert.equal(pull.body.page.states.B0.unknown,undefined); const shardNames=(await fsp.readdir(path.join(dataRoot,'world','shared-world.boards'))).sort();assert.deepEqual(shardNames,['B0.2.json','B1.3.json']); const viewport=await request('/api/realtime/send',{method:'POST',headers:auth(bob),body:JSON.stringify({presenceId:bobConnect.body.presenceId,message:{type:'viewport',minX:-10,minY:-10,maxX:10,maxY:10},afterSequence:bobConnect.body.sequence})}),after=viewport.body.sequence,started=Date.now(),pollPromise=request(`/api/realtime/poll?presenceId=${encodeURIComponent(bobConnect.body.presenceId)}&after=${after}&wait=20000`,{headers:auth(bob)});await sleep(300);await request('/api/realtime/send',{method:'POST',headers:auth(alice),body:JSON.stringify({presenceId:aliceConnect.body.presenceId,message:{type:'release',boardId:'B0'},afterSequence:aliceConnect.body.sequence})});const poll=await pollPromise;assert(Date.now()-started<3000);assert(poll.body.messages.some(message=>message.type==='claim-release'&&message.boardId==='B0')); for(let i=0;i<4;i++)assert.equal((await request('/api/realtime/connect',{method:'POST',headers:auth(alice),body:'{}'})).response.status,200);assert.equal((await request('/api/realtime/connect',{method:'POST',headers:auth(alice),body:'{}'})).response.status,503); for(let index=2;index<30;index++)assert.equal((await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:`P${index}`})})).response.status,201);assert.equal((await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:'{}'})).response.status,429); console.log('Server integrity, rebase, retention, long-poll, capacity, and rate-limit hardening passed'); }finally{await stop();await fsp.rm(dataRoot,{recursive:true,force:true})} })().catch(error=>{console.error(error);process.exitCode=1});