Clankergit add .!
This commit is contained in:
26
.devcontainer/devcontainer.json
Normal file
26
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,26 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
|
||||
{
|
||||
"name": "Existing Dockerfile",
|
||||
"build": {
|
||||
// Sets the run context to one level up instead of the .devcontainer folder.
|
||||
"context": "..",
|
||||
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
|
||||
"dockerfile": "../Dockerfile"
|
||||
}
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Uncomment the next line to run commands after the container is created.
|
||||
// "postCreateCommand": "cat /etc/os-release",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "devcontainer"
|
||||
}
|
||||
147
.gitignore
vendored
Normal file
147
.gitignore
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
.output
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Sveltekit cache directory
|
||||
.svelte-kit/
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Firebase cache directory
|
||||
.firebase/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v3
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Vite files
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.vite/
|
||||
|
||||
# ShittyServerless assets
|
||||
localstorage/
|
||||
config.js
|
||||
|
||||
# Config.js includes s3 login details, so it should not be committed to version control.
|
||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM debian:stable
|
||||
|
||||
# Install Node.js (version 22)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
EXPOSE 3000
|
||||
9
config.sample.js
Normal file
9
config.sample.js
Normal file
@@ -0,0 +1,9 @@
|
||||
var config = {
|
||||
buckregion: 'local', // Use file-based storage if bucket region is set to 'local', else use S3
|
||||
bucketname: 'functions',
|
||||
accessKeyId: 'keyhere',
|
||||
secretAccessKey: 'secretkeyhere',
|
||||
endpoint: 's3.amazonaws.com' // Your S3 endpoint
|
||||
};
|
||||
|
||||
module.exports = config
|
||||
319
index.js
Normal file
319
index.js
Normal file
@@ -0,0 +1,319 @@
|
||||
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(`<h1>ShittyServerless execution instance.</h1> If you are an end-user, contact site-admin for documentation.<br>
|
||||
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}`)
|
||||
})
|
||||
1236
package-lock.json
generated
Normal file
1236
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "shittyserverless",
|
||||
"version": "1.0.0",
|
||||
"description": "Shitty serverless framework cuz openfaas is restrictive",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "node index.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://hazzy.nonamesoft.xyz/ChattedRooms/shittyserverless.git"
|
||||
},
|
||||
"author": "ChattedRooms",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.1692.0",
|
||||
"express": "^5.1.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user