98 lines
4.1 KiB
JavaScript
98 lines
4.1 KiB
JavaScript
import http from 'node:http';
|
|
import crypto from 'node:crypto';
|
|
import { readFileSync } from 'node:fs';
|
|
import { validateJob, isValidSub, isValidRunId } from './validate.js';
|
|
|
|
const MAX_BODY = 256 * 1024;
|
|
|
|
export function readMemAvailable(path = '/proc/meminfo') {
|
|
const m = /^MemAvailable:\s+(\d+)\s+kB/m.exec(readFileSync(path, 'utf8'));
|
|
return m ? Number(m[1]) * 1024 : 0;
|
|
}
|
|
|
|
function tokenOk(header, token) {
|
|
if (!token) return false;
|
|
const m = /^Bearer (.+)$/.exec(header || '');
|
|
if (!m) return false;
|
|
const a = crypto.createHash('sha256').update(m[1]).digest();
|
|
const b = crypto.createHash('sha256').update(token).digest();
|
|
return crypto.timingSafeEqual(a, b);
|
|
}
|
|
|
|
function send(res, status, obj) {
|
|
const body = JSON.stringify(obj);
|
|
res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
|
|
res.end(body);
|
|
}
|
|
|
|
function readJson(req) {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks = [];
|
|
let size = 0;
|
|
let tooBig = false;
|
|
req.on('data', (c) => {
|
|
if (tooBig) return; // Rest verwerfen, damit die Antwort noch ausgeliefert wird
|
|
size += c.length;
|
|
if (size > MAX_BODY) { tooBig = true; chunks.length = 0; return; }
|
|
chunks.push(c);
|
|
});
|
|
req.on('end', () => {
|
|
if (tooBig) return reject(Object.assign(new Error('Anfrage zu groß'), { status: 413 }));
|
|
const raw = Buffer.concat(chunks).toString('utf8');
|
|
if (!raw) return resolve({});
|
|
try { resolve(JSON.parse(raw)); } catch { reject(Object.assign(new Error('Ungültiges JSON'), { status: 400 })); }
|
|
});
|
|
req.on('error', reject);
|
|
});
|
|
}
|
|
|
|
export function createApiServer({ config, jobs, dev, mem = readMemAvailable }) {
|
|
return http.createServer(async (req, res) => {
|
|
try {
|
|
if (!tokenOk(req.headers.authorization, config.token)) return send(res, 401, { error: 'Nicht autorisiert' });
|
|
const url = new URL(req.url, 'http://x');
|
|
const parts = url.pathname.split('/').filter(Boolean);
|
|
const method = req.method;
|
|
|
|
if (method === 'GET' && url.pathname === '/status') {
|
|
return send(res, 200, { dev: dev.list(), queue: jobs.queueLength(), memAvailable: mem() });
|
|
}
|
|
if (method === 'POST' && url.pathname === '/jobs/build') {
|
|
const v = validateJob(await readJson(req));
|
|
if (!v.ok) return send(res, 400, { error: v.error });
|
|
return send(res, 200, jobs.submit(v.job));
|
|
}
|
|
if (method === 'GET' && parts[0] === 'jobs' && parts.length === 2) {
|
|
if (!isValidRunId(parts[1])) return send(res, 400, { error: 'Ungültige Lauf-ID' });
|
|
const run = jobs.get(parts[1]);
|
|
return run ? send(res, 200, run) : send(res, 404, { error: 'Lauf nicht gefunden' });
|
|
}
|
|
if (method === 'PUT' && parts[0] === 'dev' && parts.length === 2) {
|
|
const sub = parts[1];
|
|
if (!isValidSub(sub)) return send(res, 400, { error: 'Ungültige Subdomain' });
|
|
const v = validateJob(await readJson(req));
|
|
if (!v.ok) return send(res, 400, { error: v.error });
|
|
if (v.job.sub !== sub) return send(res, 400, { error: 'Subdomain stimmt nicht überein' });
|
|
return send(res, 200, { state: (await dev.register(sub, v.job)).state });
|
|
}
|
|
if (method === 'POST' && parts[0] === 'dev' && parts.length === 3 && ['start', 'stop'].includes(parts[2])) {
|
|
const sub = parts[1];
|
|
if (!isValidSub(sub)) return send(res, 400, { error: 'Ungültige Subdomain' });
|
|
if (parts[2] === 'stop') {
|
|
await dev.stop(sub);
|
|
return send(res, 200, { ok: true });
|
|
}
|
|
const v = validateJob(await readJson(req));
|
|
if (!v.ok) return send(res, 400, { error: v.error });
|
|
if (v.job.sub !== sub) return send(res, 400, { error: 'Subdomain stimmt nicht überein' });
|
|
return send(res, 200, await dev.ensure(sub, v.job));
|
|
}
|
|
return send(res, 404, { error: 'Nicht gefunden' });
|
|
} catch (e) {
|
|
const status = e.status || 500;
|
|
if (status >= 500) console.error('Runner-Fehler:', e);
|
|
if (!res.headersSent) send(res, status, { error: status === 500 ? 'Interner Fehler' : e.message });
|
|
}
|
|
});
|
|
}
|