library-api.mjs 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344
  1. /** Same-origin read service. Production requires a gateway token, kept server-side. */
  2. import { spawn } from 'node:child_process';
  3. import { createServer } from 'node:http';
  4. import { timingSafeEqual } from 'node:crypto';
  5. import { fileURLToPath } from 'node:url';
  6. import path from 'node:path';
  7. const ROOT=path.dirname(fileURLToPath(import.meta.url));
  8. export function createLibraryApi({db,registry,python=process.env.KSF_LIBRARY_PYTHON||'python',localPreview=false,token=process.env.KSF_LIBRARY_API_TOKEN}={}) {
  9. let active=0;
  10. return async (req,res)=>{
  11. const send=(status,data)=>{res.writeHead(status,{'content-type':'application/json; charset=utf-8','cache-control':'no-store','x-content-type-options':'nosniff'});res.end(JSON.stringify(data));};
  12. if(req.method!=='POST')return send(405,{ok:false,error:{code:'METHOD_NOT_ALLOWED'}});
  13. const incoming=Buffer.from(req.headers.authorization||''),expected=Buffer.from('Bearer '+(token||''));
  14. if(!localPreview&&(!token||incoming.length!==expected.length||!timingSafeEqual(incoming,expected)))return send(401,{ok:false,error:{code:'AUTH_REQUIRED'}});
  15. if(active>=4)return send(429,{ok:false,error:{code:'LIBRARY_BUSY'}});
  16. let counted=false;
  17. try {
  18. let size=0;const chunks=[];
  19. for await(const part of req){size+=part.length;if(size>24000)throw Error('REQUEST_TOO_LARGE');chunks.push(part);}
  20. const request=JSON.parse(Buffer.concat(chunks).toString('utf8'));
  21. if(!request||typeof request!=='object'||Array.isArray(request))throw Error('INVALID_REQUEST');
  22. if(!db&&!registry)return send(503,{ok:false,error:{code:'DATABASE_NOT_INSTALLED'}});
  23. if(active>=4)return send(429,{ok:false,error:{code:'LIBRARY_BUSY'}});
  24. active++;counted=true;
  25. const data=await new Promise((resolve,reject)=>{
  26. const child=spawn(python,['-B','-X','utf8',path.join(ROOT,'voc-library/board_library.py')],{windowsHide:true,stdio:['pipe','pipe','pipe']});
  27. let output='',bytes=0;const timeout=setTimeout(()=>{child.kill();reject(Error('LIBRARY_TIMEOUT'));},25000);
  28. child.stdout.setEncoding('utf8');
  29. child.stdout.on('data',chunk=>{bytes+=Buffer.byteLength(chunk);if(bytes>5000000){child.kill();reject(Error('RESPONSE_TOO_LARGE'));}else output+=chunk;});
  30. child.stderr.resume();child.stdin.on('error',()=>{});
  31. child.on('error',()=>{clearTimeout(timeout);reject(Error('LIBRARY_RUNTIME_UNAVAILABLE'));});
  32. child.on('close',()=>{clearTimeout(timeout);try{resolve(JSON.parse(output));}catch{reject(Error('LIBRARY_READ_FAILED'));}});
  33. child.stdin.end(JSON.stringify({config:{db,registry},request}));
  34. });
  35. return send(data.ok?200:400,{...data,localPreview});
  36. }catch(e){const code=['REQUEST_TOO_LARGE','INVALID_REQUEST','LIBRARY_TIMEOUT','LIBRARY_RUNTIME_UNAVAILABLE','RESPONSE_TOO_LARGE'].includes(e.message)?e.message:'LIBRARY_READ_FAILED';return send(400,{ok:false,error:{code}});}
  37. finally{if(counted)active--;}
  38. };
  39. }
  40. if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
  41. if(!process.env.KSF_LIBRARY_API_TOKEN)throw Error('KSF_LIBRARY_API_TOKEN_REQUIRED');
  42. createServer(createLibraryApi({db:process.env.KSF_VOC_DB,registry:process.env.KSF_LIBRARY_REGISTRY})).listen(Number(process.env.KSF_LIBRARY_PORT||4180),'127.0.0.1');
  43. }