Last active
December 21, 2021 15:41
-
-
Save bmeck/fbcf80b2dbd1a0639ba53d6e75923e02 to your computer and use it in GitHub Desktop.
Revisions
-
bmeck revised this gist
Dec 21, 2021 . 1 changed file with 8 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -53,11 +53,18 @@ function gatedReadFile(filepath) { // has no reference to how auth is obtained/propagated if (!hasPermission('fs')) { // has no reference to the request / no need to propagate it throw new Error('403'); } return readFile(filepath, 'utf8'); } process.on('uncaughtExceptionMonitor', function (err) { let reqUrl = httpContext.getStore(); if (reqUrl) { console.error('Error from URL %s', reqUrl); } }); const cleanup = new FinalizationRegistry(([req, res, id, cleanup]) => { console.log('closing dropped connection to ', req.url, 'with cleanup handler id', id); cleanup(); -
bmeck created this gist
Dec 21, 2021 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,144 @@ // node [--trace-gc] local-storage-exmaple.cjs // creates a server // will drop /favicon.ico connections poorly without properly closing them // `cleanupContext` AsyncLocalStorage + `cleanup` FinalizationRegistry will clean up // will track the http request URL for errors in httpContext // will show up in error generated for bad permissions // will grant permissions in permissionsContext based upon query params / search params // need ?fs=true to make / respond with a 200 // // goto /?fs=true in browser to see it work // goto /?fs=false in browser to see it error // browsers will automatically request /favicon.ico 'use strict'; const { AsyncLocalStorage } = require('async_hooks'); const http = require('http'); const events = require('events'); const { readFile } = require('fs/promises'); // tracking what permissions we have const permissionsContext = new AsyncLocalStorage(); // tracking which HTTP context we are in const httpContext = new AsyncLocalStorage(); const getSearchParams = (reqUrl) => { const url = new URL(reqUrl, 'http://invalid.invalid/'); return url.searchParams; } /** * FOR DEMO PURPOSES ONLY, NEVER DO THIS * Use real auth methods and not search params * @param req */ async function auth(req, fn) { // NO NO NO NO NO let permissions = {}; for (const [key, value] of getSearchParams(req.url).entries()) { permissions[key] = value; } permissionsContext.run(permissions, fn); } function hasPermission(key) { return permissionsContext.getStore()?.[key] === 'true'; } /** * needs ?fs=true * @param filepath * @returns */ function gatedReadFile(filepath) { // has no reference to how auth is obtained/propagated if (!hasPermission('fs')) { // has no reference to the request / no need to propagate it throw new Error('403 ' + httpContext.getStore()); } return readFile(filepath, 'utf8'); } const cleanup = new FinalizationRegistry(([req, res, id, cleanup]) => { console.log('closing dropped connection to ', req.url, 'with cleanup handler id', id); cleanup(); try { res.writeHead(500); res.end(); } catch { // this is normal } }); // Garbage generator to keep GC running periodically setInterval(() => [].concat(1), 0); setInterval(() => [].concat(1), 0); setInterval(() => [].concat(1), 0); setInterval(() => [].concat(1), 0); let gcId = 1; const cleanupContext = new AsyncLocalStorage(); /** * @param {import('http').IncomingMessage} req * @param {import('http').ServerResponse} res * @returns */ function setCleanup(req, res, fn) { let id = gcId++; // {id} is our "token" // don't directly hold onto {id}, it won't be able to GC then let GC = new WeakRef({id}); // data to associate with the token let held = [req, res, id, () => { res.off('finish', unregister); }]; console.log(req.url, 'assigned cleanup handler', id); function unregister() { console.log(req.url, 'unregistered cleanup handler', id); cleanup.unregister(GC.deref()); } // setup when the token deallocs, fire handler cleanup.register(GC.deref(), held, GC.deref()); res.on('finish', unregister); // Force the token to stay alive for all async work spawned from the current task cleanupContext.run(GC.deref(), fn); } /** * @param {import('http').IncomingMessage} req * @param {import('http').ServerResponse} res * @returns */ function handleHTTP(req, res) { setCleanup(req, res, () => { if (req.url === '/favicon.ico') { // uh oh, forgot to close (don't worry the Finalization Registry handles it) setTimeout(() => { // keep the req, res alive WAAAAY too long // should see a GC entry before this console.log('can cleanup /favicon.ico') }, 0); return; } httpContext.run(req.url, () => { auth(req, () => { console.log('%s %s', req.method, req.url); // "simulate" a work queue setTimeout(doWork, 1, res); }); }); }); } async function doWork(res) { res.end(await gatedReadFile(__filename)); } async function main() { const server = http.createServer(handleHTTP).listen(process.env.PORT || 0) await events.once(server, 'listening'); console.log('Listening on', server.address()); console.log(`http://127.0.0.1:${server.address().port}`) } main();