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