const express = require('express') const { spawn } = require('child_process'); const readline = require('readline'); const AWS = require('aws-sdk'); const config = require('./config.js'); const fs = require('fs'); const path = require('path'); const { get } = require('http'); const app = express(); const port = 3000; // FILE FUNCS // Configure S3 client if not using local let s3; if (config.buckregion !== 'local') { // assuming 'local' means local file system s3 = new AWS.S3({ accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey, endpoint: config.endpoint, s3ForcePathStyle: true, // needed for self-hosted S3 like MinIO or garage }); } else { console.log("!! Using local storage for files. This is not recommended for production.") } function getFileFromStorage(loc) { return new Promise((resolve, reject) => { if (config.buckregion === 'local') { fs.readFile(path.join("localstorage", loc), { encoding: 'utf-8' }, (err, data) => { if (err) return reject(err); resolve(data); }); } else { // S3 access const params = { Bucket: config.bucketname, Key: loc, // loc is the file key in S3 }; s3.getObject(params, (err, data) => { if (err) return reject(err); resolve(data.Body.toString('utf-8')); }); } }); } function writeLocalFile(loc, data) { return new Promise((resolve, reject) => { fs.writeFile(loc, data, (err) => { if (err) return reject(err); resolve(); }); }); } app.get('/', (req, res) => { res.send(`

ShittyServerless execution instance.

If you are an end-user, contact site-admin for documentation.
If you are a developer, deploy your serverless functions by placing files in the garage webui manually.`); }) const exitCodeLookup = { 0: 200, 1: 500, 2: 403, 3: 401, 4: 404, } // Lookup table to map exit codes to HTTP status codes function execFileAsync(req,res, command, location, opts = {}) { // opts: { timeoutMs: number } optional const timeoutMs = opts.timeoutMs || Infinity; // default no timeout let killTimer; // to hold timeout timer return new Promise((resolve, reject) => { let finished = false; function onceFinish(err) { if (finished) return; finished = true; try{clearTimeout(killTimer);console.log("Cleared timer")}catch(e){} if (err) { try { res.status(500).end(); } catch (e) {} reject(err); } else { resolve(); } } (async () => { try { // --- prepare temp file (keeps your original logic) --- const tempDir = '/tmp'; const fileName = path.basename(location[0]); const tempFilePath = path.join(tempDir, fileName); const contents = await getFileFromStorage(location[0]); await writeLocalFile(tempFilePath, contents); // --- spawn process --- const child = spawn(command, [tempFilePath]); // make sure we DONT close stdin (we need to send data to the child for it to have reactiveness) child.on('error', (err) => { console.error('child process error', err); onceFinish(new Error('Failed to spawn child process: ' + err.message)); }); // safety timeout to avoid indefinite hangs killTimer = setTimeout(() => { if (finished) return; console.error(`child timeout after ${timeoutMs}ms — killing pid ${child.pid}`); try { child.kill('SIGKILL'); } catch (e) {} onceFinish(new Error(`Child process timeout after ${timeoutMs}ms`)); }, timeoutMs); // --- streaming header parser --- let isReadingHeaders = true; let headerBuf = ''; // string buffer for header parsing let headers = { 'Content-Type': 'text/plain' }; let statusCode = 200; function parseHeadersAndFlush(remainder) { const lines = headerBuf.split(/\r?\n/); for (const line of lines) { if (!line.trim()) continue; // if line looks like "Key: value" const idx = line.indexOf(':'); if (idx > -1) { const key = line.slice(0, idx).trim(); const val = line.slice(idx + 1).trim(); // treat status header specially if you want if (key.toLowerCase() === 'status') { const parsed = parseInt(val, 10); if (!Number.isNaN(parsed)) statusCode = parsed; } else { headers[key] = val; } } } // Send headers and write remainder try { res.writeHead(statusCode, headers); } catch (e) { console.warn('res.writeHead failed', e); } if (remainder && remainder.length) { res.write(remainder); } } // We will listen on stdout data and look for the token. child.stdout.on('data', (chunk) => { if (finished) return; const str = chunk.toString('utf8'); if (isReadingHeaders) { headerBuf += str; // Look for the token on its own line anywhere in headerBuf const token = '\n!!STARTBODY'; let index = headerBuf.indexOf(token); // also allow token at very start (no preceding newline) if (index === -1 && headerBuf.indexOf('!!STARTBODY') === 0) { index = 0; } if (headerBuf.endsWith("!!RECVDATA")) { console.log("Child is requesting data from us..."); child.stdin.write(JSON.stringify({ method: req.method, headers: req.headers, query: req.query, params: req.params })); child.stdin.write("\n"); // newline to signal end of JSON, because child most likely uses input() or readline() headerBuf = headerBuf.slice(0, headerBuf.length - "!!RECVDATA".length); // remove the token, to avoid cluttering headerspace return; // wait for more data } if (index !== -1) { // header portion is everything before token (strip a preceding newline) let headerPart = headerBuf.slice(0, index); // remainder after token let remainder = headerBuf.slice(index + token.length); // If token was at start without the leading \n, we've included token; handle that case if (headerPart.startsWith('\n')) headerPart = headerPart.slice(1); // parse headerPart lines headerBuf = headerPart; parseHeadersAndFlush(remainder); // switch mode isReadingHeaders = false; } else { // if no token yet, keep buffering — but to avoid unbounded growth, cap buffer size const MAX_HEADER_BUF = 64 * 1024; // 64KB if (headerBuf.length > MAX_HEADER_BUF) { // give up and treat whole buffer as headers parseHeadersAndFlush(''); isReadingHeaders = false; } } } else { // already reading body — stream straight to response res.write(str); } }); // also forward stderr to console (and optionally to response) child.stderr.on('data', (data) => { console.error(`stderr: ${data.toString()}`); }); child.on('close', (code) => { // If we never left header mode, send a helpful message if (isReadingHeaders) { try { res.writeHead(200, headers); res.write("No body content. Double check that the child process emits the delimiter `!!STARTBODY` followed by a newline.\n"); } catch (e) { console.warn('writeHead on close failed', e); } } console.log(`child process exited with code ${code}`); const mappedStatus = exitCodeLookup[code] || 500; console.log(`Mapping exit code ${code} to HTTP status ${mappedStatus}`); try { res.status(mappedStatus); res.end(); } catch (e) {console.log(e)} // Remove temp file try { fs.unlink(tempFilePath, (err) => { if (err) { console.warn('Failed to remove temp file', tempFilePath, err); } else { console.log('Temp file removed', tempFilePath); } }); } catch (e) { console.warn('unlink failed', e); } onceFinish(null,code); }); } catch (err) { onceFinish(err); } })(); }); } const lookup = { sh: 'bash', py: 'python3', js: 'node', } app.get('/*path', async (req, res) => { const filePath = req.path.replace(/^\/+/, ''); // remove leading slash(s) console.log(`[request] filePath="${filePath}"`); if (!filePath) { return res.status(400).send('No file requested'); } // default timeout (ms) for execFileAsync; tune as needed const execOpts = { timeoutMs: 30_000 }; try { if (filePath.includes('.')) { const ext = filePath.split('.').pop(); console.log(`[lookup] requested ext="${ext}"`); if (lookup[ext]) { await execFileAsync(req,res, lookup[ext], [filePath], execOpts); return; // response handled by execFileAsync } else { console.log(`[lookup] no handler for extension "${ext}"`); } } // Brute-force check (ensure fs checks the correct cwd or use absolute paths) let found = false; for (const ext in lookup) { const candidate = filePath + '.' + ext; console.log(`[bruteforce] checking ${candidate}`); try{ await getFileFromStorage(candidate); console.log(`[bruteforce] found ${candidate} -> handler ${lookup[ext]}`); await execFileAsync(req,res, lookup[ext], [candidate], execOpts); found = true; break; } catch {continue;} } if (!found) { if (!res.headersSent) { res.status(404).send("No matching file found for serverless execution"); } else { res.end(); } return; } } catch (err) { console.error('[execFileAsync] error:', err && err.stack ? err.stack : err); // If execFileAsync already sent headers/body, don't try to send another response if (!res.headersSent) { res.status(500).send("Serverless function failed to execute"); } else { try { res.end(); } catch (e) {} } return; } }); app.listen(port, () => { console.log(`Example app listening on port ${port}`) })