├── .npmrc ├── cjs ├── package.json └── index.js ├── test ├── package.json ├── index.js ├── test.js ├── index.html └── test.html ├── .gitignore ├── .npmignore ├── electroff-head.jpg ├── examples └── oled │ ├── oled.jpg │ ├── package.json │ ├── index.js │ ├── README.md │ └── public │ └── index.html ├── LICENSE ├── package.json ├── client └── index.js ├── esm └── index.js └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .travis.yml 4 | node_modules/ 5 | rollup/ 6 | test/ 7 | -------------------------------------------------------------------------------- /electroff-head.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/electroff/HEAD/electroff-head.jpg -------------------------------------------------------------------------------- /examples/oled/oled.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/electroff/HEAD/examples/oled/oled.jpg -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const {PORT = 3000} = process.env; 2 | 3 | const express = require('express'); 4 | const electroff = require('../'); 5 | 6 | const app = express(); 7 | app.use(electroff); 8 | app.use(express.static(__dirname)); 9 | app.listen(PORT, () => console.log(`http://localhost:${PORT}`)); 10 | -------------------------------------------------------------------------------- /examples/oled/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electroff-oled", 3 | "private": true, 4 | "dependencies": { 5 | "electroff": "latest", 6 | "express": "^4.17.1", 7 | "johnny-five": "^2.0.0", 8 | "oled-font-5x7": "latest", 9 | "oled-js": "latest", 10 | "raspi-io": "^11.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/oled/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {PORT = 3000} = process.env; 4 | 5 | const express = require('express'); 6 | const electroff = require('electroff'); 7 | 8 | const app = express(); 9 | app.use(electroff); 10 | app.use(express.static(`${__dirname}/public`)); 11 | app.listen(PORT, () => console.log(`http://localhost:${PORT}`)); 12 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const {readFileSync} = require('fs'); 2 | const {join} = require('path'); 3 | 4 | const electroff = require('../cjs'); 5 | 6 | const index = readFileSync(join(__dirname, 'index.html')); 7 | 8 | require('http') 9 | .createServer((request, response) => { 10 | if (electroff(request, response)) return; 11 | response.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'}); 12 | response.end(index); 13 | }) 14 | .listen(8080, () => console.log('http://localhost:8080/')); 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Electroff Basic Test 7 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/oled/README.md: -------------------------------------------------------------------------------- 1 | # Raspberry Pi Oled Demo 2 | 3 | - - - 4 | 5 | **Announcement**: electroff can be fully replaced by [coincident/server](https://github.com/WebReflection/coincident-oled#readme)! 6 | 7 | - - - 8 | 9 | Install the _pigpio_ first, and **reboot** the board: 10 | 11 | * in ArchLinux via [pigpio](https://aur.archlinux.org/packages/pigpio/) - Please note this requires a dedicated raspberrypi linux kernel 12 | * in Debian or Raspbian via `sudo apt install pigpio` 13 | 14 | Download [package.json](./package.json) and [index.js](./index.js) in any local folder. 15 | 16 | Create a `public` folder, via `mkdir -p public`, and download [index.html](./public/index.html) in there. 17 | 18 | Find out which IP address your PI has, i.e. via `ip addr`. 19 | 20 | Start the demo via `sudo DEBUG=1 node ./index.js` and reach the Pi through any browser on the specified port, i.e. `http://192.168.1.23:3000/`. 21 | 22 | ![oled image example](./oled.jpg) 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electroff", 3 | "version": "0.4.1", 4 | "description": "A cross browser, electron-less helper, for IoT projects and standalone applications", 5 | "main": "./cjs/index.js", 6 | "scripts": { 7 | "build": "npm run cjs && npm run test", 8 | "cjs": "ascjs --no-default esm cjs", 9 | "test": "DEBUG=1 node test/index.js" 10 | }, 11 | "keywords": [ 12 | "electron", 13 | "alternative", 14 | "cross", 15 | "browser" 16 | ], 17 | "author": "Andrea Giammarchi", 18 | "license": "ISC", 19 | "devDependencies": { 20 | "ascjs": "^4.0.1", 21 | "express": "^4.17.1" 22 | }, 23 | "module": "./esm/index.js", 24 | "type": "module", 25 | "exports": { 26 | "import": "./esm/index.js", 27 | "default": "./cjs/index.js" 28 | }, 29 | "dependencies": { 30 | "flatted": "^3.1.0", 31 | "umeta": "^0.2.4" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/WebReflection/electroff.git" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/WebReflection/electroff/issues" 39 | }, 40 | "homepage": "https://github.com/WebReflection/electroff#readme" 41 | } 42 | -------------------------------------------------------------------------------- /examples/oled/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Oled Update 5 | 6 | 7 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | electroff 7 | 8 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | const electroff = (function (fetch) {'use strict'; 2 | 3 | const self = {}; 4 | "{{Flatted}}"; 5 | 6 | const UID = '{{UID}}'; 7 | const channel = '{{channel}}'; 8 | 9 | const secret = Symbol('electroff'); 10 | const {parse} = self.Flatted; 11 | const {stringify} = JSON; 12 | 13 | const apply = (target, self, args) => 14 | `${target()}.apply(${json(self)},${json(args)})` 15 | ; 16 | 17 | let NetworkOK = true; 18 | const exec = body => fetch('electroff', { 19 | headers: {'Content-Type': 'application/json;charset=utf-8'}, 20 | method: 'POST', 21 | body: json(body) 22 | }).then(b => b.text()).then(parse, (e = {message: 'Network Error'}) => { 23 | NetworkOK = false; 24 | document.documentElement.innerHTML = ` 25 | 26 | 27 | 28 | Network Error 29 | 30 | 31 | 32 | 33 |

Network Error

34 |

35 | Please 36 |

37 | 38 | 39 | `.trim(); 40 | return {error: e.message}; 41 | }); 42 | 43 | const json = any => stringify(any, (_, any) => { 44 | if (typeof any === 'function') 45 | return `(\xFF${any[secret] || any}\xFF)`; 46 | return any; 47 | }); 48 | 49 | (function poll() { 50 | if (NetworkOK) 51 | setTimeout( 52 | () => exec({UID, channel, code: 'true'}).then(poll), 53 | 60000 54 | ); 55 | }()); 56 | 57 | addEventListener('beforeunload', e => { 58 | navigator.sendBeacon('electroff', stringify({UID, channel, exit: true})); 59 | }); 60 | 61 | let calls = 0; 62 | let once = '{{once}}'; 63 | return function electroff(fn) { 64 | // avoid invoking CommonJS/electroff more than once 65 | // whenever the ELECTROFF_ONCE flag is passed 66 | if (once && calls++) 67 | throw new Error('Unhautorized script'); 68 | 69 | let instances = 0; 70 | let run = Promise.resolve(); 71 | 72 | const evaluate = code => (run = run.then(() => 73 | exec({UID, channel, code}).then(response => { 74 | const {result, error} = response; 75 | if (error) 76 | return Promise.reject(new Error(error)); 77 | if (result && result[channel]) { 78 | const global = result[channel]; 79 | return new Proxy(() => global, $handler); 80 | } 81 | return result; 82 | }) 83 | )); 84 | 85 | const handler = { 86 | apply(target, self, args) { 87 | return new Proxy( 88 | function () { 89 | return apply(target, self, args); 90 | }, 91 | handler 92 | ); 93 | }, 94 | construct(target, args) { 95 | const ref = `global['${UID}']['\x00'][${instances++}]`; 96 | const text = `(${ref}||(${ref}=new(${target()})(...${json(args)})))`; 97 | return new Proxy(() => text, handler); 98 | }, 99 | get(target, key) { 100 | switch (key) { 101 | case secret: 102 | case 'toJSON': 103 | return target(); 104 | case 'apply': 105 | return (self, args) => this.apply(target, self, args); 106 | case 'bind': 107 | return (self, ...args) => (...extras) => new Proxy( 108 | function () { 109 | return apply(target, self, args.concat(extras)); 110 | }, 111 | handler 112 | ); 113 | case 'call': 114 | return (self, ...args) => this.apply(target, self, args); 115 | case 'then': 116 | return fn => evaluate(target()).then(fn); 117 | } 118 | return new Proxy( 119 | function () { 120 | return `${target()}[${stringify(key)}]`; 121 | }, 122 | handler 123 | ); 124 | }, 125 | has(target, key) { 126 | throw new Error(`invalid "${key}" in ${target()} operation`); 127 | }, 128 | set(target, k, v) { 129 | evaluate(`${target()}[${stringify(k)}]=${json(v)}`); 130 | return true; 131 | } 132 | }; 133 | 134 | const $handler = { 135 | ...handler, 136 | get(target, key) { 137 | return key === 'then' ? void 0 : handler.get(target, key); 138 | } 139 | }; 140 | 141 | fn({ 142 | __dirname: '{{__dirname}}', 143 | global: new Proxy( 144 | () => `global['${UID}']`, 145 | handler 146 | ), 147 | remove: target => exec({UID, channel, code: `delete ${target[secret]}`}) 148 | .then(({result}) => result), 149 | require: module => new Proxy( 150 | function () { 151 | return `require(${stringify(module)})`; 152 | }, 153 | handler 154 | ), 155 | until: emitter => new Proxy( 156 | () => `(e=>((e.is||(e.is=t=>new Promise($=>{e.once(t,$)}))),e))(${ 157 | emitter[secret] 158 | })`, 159 | handler 160 | ) 161 | }); 162 | }; 163 | }(fetch)); 164 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ISC License 3 | * Copyright (c) 2020, Andrea Giammarchi, @WebReflection 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | * PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | import crypto from 'crypto'; 19 | import {readFileSync} from 'fs'; 20 | import {basename, dirname, join} from 'path'; 21 | import {env} from 'process'; 22 | import vm from 'vm'; 23 | 24 | import {stringify} from 'flatted'; 25 | 26 | import umeta from 'umeta'; 27 | const {dirName, require: $require} = umeta(import.meta); 28 | 29 | const {isArray} = Array; 30 | const {create, getPrototypeOf} = Object; 31 | const {parse} = JSON; 32 | 33 | const cache = new Map; 34 | const rand = length => crypto.randomBytes(length).toString('hex'); 35 | 36 | const revive = /"\(\xFF([^\2]+?)(\xFF\)")/g; 37 | const callback = (_, $1) => parse(`"${$1}"`); 38 | 39 | const CHANNEL = rand(32); 40 | const EXPIRE = 300000; // expires in 5 minutes 41 | const X00 = '\x00'; 42 | const DEBUG = /^(?:1|y|yes|true)$/i.test(env.DEBUG); 43 | 44 | const ELECTROFF_ONCE = /^(?:1|y|yes|true)$/i.test(env.ELECTROFF_ONCE); 45 | 46 | const cleanup = () => { 47 | const now = Date.now(); 48 | const drop = []; 49 | cache.forEach((value, key) => { 50 | if (value < now) 51 | drop.push(key); 52 | }); 53 | const {length} = drop; 54 | if (0 < length) { 55 | if (DEBUG) 56 | console.log( 57 | `purged ${length} client${ 58 | length === 1 ? '' : 's' 59 | } - total ${cache.size}` 60 | ); 61 | drop.forEach(UID => { 62 | cache.delete(UID); 63 | delete sandbox.global[UID]; 64 | }); 65 | } 66 | }; 67 | 68 | const js = ''.replace.call( 69 | readFileSync(join(dirName, '..', 'client', 'index.js')), 70 | '{{channel}}', 71 | CHANNEL 72 | ).replace( 73 | '{{__dirname}}', 74 | dirName 75 | ).replace( 76 | /('|"|`)\{\{once\}\}\1/, 77 | ELECTROFF_ONCE 78 | ).replace( 79 | '"{{Flatted}}"', 80 | readFileSync( 81 | join(dirname($require.resolve('flatted')), '..', 'es.js') 82 | ).toString() 83 | ); 84 | 85 | const sandbox = vm.createContext({ 86 | global: create(null), 87 | require: $require, 88 | Buffer, 89 | console, 90 | setTimeout, 91 | setInterval, 92 | clearTimeout, 93 | clearInterval, 94 | setImmediate, 95 | clearImmediate 96 | }); 97 | 98 | const ok = (response, content) => { 99 | response.writeHead(200, { 100 | 'Content-Type': 'text/plain;charset=utf-8' 101 | }); 102 | response.end(stringify(content)); 103 | }; 104 | 105 | export default (request, response, next) => { 106 | const {method, url} = request; 107 | if (/^electroff(\?module)?$/.test(basename(url))) { 108 | const {$1: asModule} = RegExp; 109 | if (method === 'POST') { 110 | const data = []; 111 | request.on('data', chunk => { 112 | data.push(chunk); 113 | }); 114 | request.on('end', async () => { 115 | try { 116 | const {UID, channel, code, exit} = parse(data.join('')); 117 | if (channel === CHANNEL && UID) { 118 | cache.set(UID, Date.now() + EXPIRE); 119 | cleanup(); 120 | const exec = (code || '').replace(revive, callback); 121 | if (!(UID in sandbox.global)) { 122 | sandbox.global[UID] = {[X00]: create(null)}; 123 | if (DEBUG) 124 | console.log(`created 1 client - total ${cache.size}`); 125 | } 126 | if (exit) { 127 | cache.delete(UID); 128 | delete sandbox.global[UID]; 129 | ok(response, ''); 130 | if (DEBUG) 131 | console.log(`removed 1 client - total ${cache.size}`); 132 | return; 133 | } 134 | 135 | // YOLO 136 | vm.runInContext( 137 | `try{global['${X00}']={result:(${exec})}} 138 | catch({message}){global['${X00}']={error:message}}`, 139 | sandbox 140 | ); 141 | 142 | const {result, error} = sandbox.global[X00]; 143 | sandbox.global[X00] = null; 144 | if (error) { 145 | ok(response, {error}); 146 | if (DEBUG) 147 | console.error(`unable to evaluate: ${exec}`); 148 | } 149 | else { 150 | try { 151 | result.then( 152 | result => { 153 | if (result instanceof Buffer) 154 | result = result.toString('utf-8'); 155 | ok(response, {result}); 156 | }, 157 | e => { 158 | ok(response, {error: e.message}); 159 | if (DEBUG) 160 | console.error(`unable to resolve: ${exec}`); 161 | } 162 | ); 163 | } 164 | catch (e) { 165 | if (typeof result === 'object') { 166 | switch (!!result) { 167 | case isArray(result): 168 | case !getPrototypeOf(getPrototypeOf(result)): 169 | case result instanceof Date: 170 | break; 171 | default: 172 | const instances = sandbox.global[UID][X00]; 173 | for (const key in instances) { 174 | if (instances[key] === result) { 175 | ok(response, { 176 | result: { 177 | [CHANNEL]: `global['${UID}']['${X00}']['${key}']` 178 | } 179 | }); 180 | return; 181 | } 182 | } 183 | } 184 | } 185 | ok(response, {result}); 186 | } 187 | } 188 | } 189 | else { 190 | response.writeHead(403); 191 | response.end(); 192 | if (DEBUG) 193 | console.error( 194 | channel ? `unauthorized client` : `unauthorized request` 195 | ); 196 | } 197 | } catch (e) { 198 | response.writeHead(500); 199 | response.end(); 200 | if (DEBUG) 201 | console.error(`internal server error`, e); 202 | } 203 | }); 204 | } 205 | else { 206 | if (ELECTROFF_ONCE && !asModule) { 207 | response.writeHead(403); 208 | response.end(); 209 | if (DEBUG) 210 | console.error(`client asked for a global electroff`); 211 | } 212 | else { 213 | response.writeHead(200, { 214 | 'Cache-Control': 'no-store', 215 | 'Content-Type': 'application/javascript;charset=utf-8' 216 | }); 217 | response.end( 218 | js.replace('{{UID}}', rand(16)).concat( 219 | asModule ? 'export default electroff;' : '' 220 | ) 221 | ); 222 | } 223 | } 224 | return true; 225 | } 226 | try { return false; } 227 | finally { 228 | if (next) 229 | next(); 230 | } 231 | }; 232 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! 3 | * ISC License 4 | * Copyright (c) 2020, Andrea Giammarchi, @WebReflection 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 12 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 14 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 15 | * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 16 | * PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | const crypto = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('crypto')); 20 | const {readFileSync} = require('fs'); 21 | const {basename, dirname, join} = require('path'); 22 | const {env} = require('process'); 23 | const vm = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('vm')); 24 | 25 | const {stringify} = require('flatted'); 26 | 27 | const umeta = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('umeta')); 28 | const {dirName, require: $require} = umeta(({url: require('url').pathToFileURL(__filename).href})); 29 | 30 | const {isArray} = Array; 31 | const {create, getPrototypeOf} = Object; 32 | const {parse} = JSON; 33 | 34 | const cache = new Map; 35 | const rand = length => crypto.randomBytes(length).toString('hex'); 36 | 37 | const revive = /"\(\xFF([^\2]+?)(\xFF\)")/g; 38 | const callback = (_, $1) => parse(`"${$1}"`); 39 | 40 | const CHANNEL = rand(32); 41 | const EXPIRE = 300000; // expires in 5 minutes 42 | const X00 = '\x00'; 43 | const DEBUG = /^(?:1|y|yes|true)$/i.test(env.DEBUG); 44 | 45 | const ELECTROFF_ONCE = /^(?:1|y|yes|true)$/i.test(env.ELECTROFF_ONCE); 46 | 47 | const cleanup = () => { 48 | const now = Date.now(); 49 | const drop = []; 50 | cache.forEach((value, key) => { 51 | if (value < now) 52 | drop.push(key); 53 | }); 54 | const {length} = drop; 55 | if (0 < length) { 56 | if (DEBUG) 57 | console.log( 58 | `purged ${length} client${ 59 | length === 1 ? '' : 's' 60 | } - total ${cache.size}` 61 | ); 62 | drop.forEach(UID => { 63 | cache.delete(UID); 64 | delete sandbox.global[UID]; 65 | }); 66 | } 67 | }; 68 | 69 | const js = ''.replace.call( 70 | readFileSync(join(dirName, '..', 'client', 'index.js')), 71 | '{{channel}}', 72 | CHANNEL 73 | ).replace( 74 | '{{__dirname}}', 75 | dirName 76 | ).replace( 77 | /('|"|`)\{\{once\}\}\1/, 78 | ELECTROFF_ONCE 79 | ).replace( 80 | '"{{Flatted}}"', 81 | readFileSync( 82 | join(dirname($require.resolve('flatted')), '..', 'es.js') 83 | ).toString() 84 | ); 85 | 86 | const sandbox = vm.createContext({ 87 | global: create(null), 88 | require: $require, 89 | Buffer, 90 | console, 91 | setTimeout, 92 | setInterval, 93 | clearTimeout, 94 | clearInterval, 95 | setImmediate, 96 | clearImmediate 97 | }); 98 | 99 | const ok = (response, content) => { 100 | response.writeHead(200, { 101 | 'Content-Type': 'text/plain;charset=utf-8' 102 | }); 103 | response.end(stringify(content)); 104 | }; 105 | 106 | module.exports = (request, response, next) => { 107 | const {method, url} = request; 108 | if (/^electroff(\?module)?$/.test(basename(url))) { 109 | const {$1: asModule} = RegExp; 110 | if (method === 'POST') { 111 | const data = []; 112 | request.on('data', chunk => { 113 | data.push(chunk); 114 | }); 115 | request.on('end', async () => { 116 | try { 117 | const {UID, channel, code, exit} = parse(data.join('')); 118 | if (channel === CHANNEL && UID) { 119 | cache.set(UID, Date.now() + EXPIRE); 120 | cleanup(); 121 | const exec = (code || '').replace(revive, callback); 122 | if (!(UID in sandbox.global)) { 123 | sandbox.global[UID] = {[X00]: create(null)}; 124 | if (DEBUG) 125 | console.log(`created 1 client - total ${cache.size}`); 126 | } 127 | if (exit) { 128 | cache.delete(UID); 129 | delete sandbox.global[UID]; 130 | ok(response, ''); 131 | if (DEBUG) 132 | console.log(`removed 1 client - total ${cache.size}`); 133 | return; 134 | } 135 | 136 | // YOLO 137 | vm.runInContext( 138 | `try{global['${X00}']={result:(${exec})}} 139 | catch({message}){global['${X00}']={error:message}}`, 140 | sandbox 141 | ); 142 | 143 | const {result, error} = sandbox.global[X00]; 144 | sandbox.global[X00] = null; 145 | if (error) { 146 | ok(response, {error}); 147 | if (DEBUG) 148 | console.error(`unable to evaluate: ${exec}`); 149 | } 150 | else { 151 | try { 152 | result.then( 153 | result => { 154 | if (result instanceof Buffer) 155 | result = result.toString('utf-8'); 156 | ok(response, {result}); 157 | }, 158 | e => { 159 | ok(response, {error: e.message}); 160 | if (DEBUG) 161 | console.error(`unable to resolve: ${exec}`); 162 | } 163 | ); 164 | } 165 | catch (e) { 166 | if (typeof result === 'object') { 167 | switch (!!result) { 168 | case isArray(result): 169 | case !getPrototypeOf(getPrototypeOf(result)): 170 | case result instanceof Date: 171 | break; 172 | default: 173 | const instances = sandbox.global[UID][X00]; 174 | for (const key in instances) { 175 | if (instances[key] === result) { 176 | ok(response, { 177 | result: { 178 | [CHANNEL]: `global['${UID}']['${X00}']['${key}']` 179 | } 180 | }); 181 | return; 182 | } 183 | } 184 | } 185 | } 186 | ok(response, {result}); 187 | } 188 | } 189 | } 190 | else { 191 | response.writeHead(403); 192 | response.end(); 193 | if (DEBUG) 194 | console.error( 195 | channel ? `unauthorized client` : `unauthorized request` 196 | ); 197 | } 198 | } catch (e) { 199 | response.writeHead(500); 200 | response.end(); 201 | if (DEBUG) 202 | console.error(`internal server error`, e); 203 | } 204 | }); 205 | } 206 | else { 207 | if (ELECTROFF_ONCE && !asModule) { 208 | response.writeHead(403); 209 | response.end(); 210 | if (DEBUG) 211 | console.error(`client asked for a global electroff`); 212 | } 213 | else { 214 | response.writeHead(200, { 215 | 'Cache-Control': 'no-store', 216 | 'Content-Type': 'application/javascript;charset=utf-8' 217 | }); 218 | response.end( 219 | js.replace('{{UID}}', rand(16)).concat( 220 | asModule ? 'export default electroff;' : '' 221 | ) 222 | ); 223 | } 224 | } 225 | return true; 226 | } 227 | try { return false; } 228 | finally { 229 | if (next) 230 | next(); 231 | } 232 | }; 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electroff 2 | 3 | ![Raspberry Pi Oled](./electroff-head.jpg) 4 | 5 | A cross browser, electron-less helper, for **IoT** projects and **standalone** applications. 6 | 7 | With this module, you can run arbitrary _Node.js_ code from the client, from any browser, and *without* needing [electron](https://www.electronjs.org/). 8 | 9 | 10 | ## 📣 Announcement 11 | 12 | electroff can be fully replaced by [coincident/server](https://github.com/WebReflection/coincident-oled#readme)! 13 | 14 | - - - 15 | 16 | Looking for a lighter, faster, much safer, yet slightly more limited alternative? Try **[proxied-node](https://github.com/WebReflection/proxied-node#readme)** out instead. 17 | 18 | 19 | ### Community 20 | 21 | Please ask questions in the [dedicated forum](https://webreflection.boards.net/) to help the community around this project grow ♥ 22 | 23 | --- 24 | 25 | ## Getting Started 26 | 27 | Considering the following `app.js` file content: 28 | 29 | ```js 30 | const {PORT = 3000} = process.env; 31 | 32 | const express = require('express'); 33 | const electroff = require('electroff'); 34 | 35 | const app = express(); 36 | app.use(electroff); 37 | app.use(express.static(`${__dirname}/public`)); 38 | app.listen(PORT, () => console.log(`http://localhost:${PORT}`)); 39 | ``` 40 | 41 | The `public/index.html` folder can contain something like this: 42 | 43 | ```html 44 | 45 | 46 | 47 | 48 | 49 | Electroff Basic Test 50 | 66 | 67 | 68 | ``` 69 | 70 | 71 | ### Helpers passed as callback object / API 72 | 73 | * `require`, usable to require any module or file, same as you would do in _CommonJS_ 74 | * `global`, usable to share a mirrored state across multiple `electroff(...)` calls (but not shared across multiple clients) 75 | * `remove`, usable to remove instances when these are not available anymore (a _WeakRef_ implementation is coming soon) 76 | * `__dirname`, which points at the _Node.js_ path that is currently running the module 77 | * `until`, usable to `await` emitters events on the client-side (read more in F.A.Q.) 78 | 79 | 80 | 81 | ## F.A.Q. 82 | 83 |
84 | How does it work? 85 |
86 | 87 | The _JS_ on the page is exactly like any regular _JS_, but anything referencing _Node.js_ environment, through any `require(...)`, is executed on a shared *sandbox* in _Node.js_, where each user gets its own *global* namespace a part. 88 | 89 | Such *sandbox* is in charge of executing code from the client, but only when the client *await* some value. 90 | 91 | ```js 92 | const {debug} = require('process').features; 93 | console.log('debug is', await debug); 94 | 95 | const {join} = require('path'); 96 | const {readFile} = require('fs').promises; 97 | const content = await readFile(join(__dirname, 'public', 'index.html')); 98 | console.log(content); 99 | ``` 100 | 101 | **In depth**: every time we `await something` in _JS_, an implicit lookup for the `.then(...)` method is performed, and that's when *electroff* can perform a fancy client/server asynchronous interaction, through all the paths reached through the various references, which are nothing more than _Proxies_ with special abilities. 102 | 103 | In few words, the following code: 104 | ```js 105 | await require('fs').promises.readFile('file.txt'); 106 | ``` 107 | 108 | would evaluated, within the _vm_ sandbox, the following code: 109 | ```js 110 | await require("fs").promises.readFile.apply( 111 | require("fs").promises, 112 | ["test.txt"] 113 | ) 114 | ``` 115 | 116 | All operations are inevitably repeated because every single `.property` access, `.method(...)` invoke, or even `new module.Thing(...)`, is a branch of the code a part. 117 | 118 | ### The foreign vs local scope 119 | 120 | It is important to keep in mind that there is a huge difference between _foreign_ code, and _scoped_ code, where _foreign_ code cannot reach _scoped_ code, and vive-versa. 121 | ```js 122 | electroff(async ({require}) => { 123 | // local scope code 124 | const num = Math.random(); 125 | 126 | // foreign code (needs to be awaited) 127 | const {EventEmitter} = require('events'); 128 | const ee = await new EventEmitter; 129 | await ee.on('stuff', async function (value) { 130 | // nothing in this scope can reach 131 | // `num`, as example, is not accessible 132 | // and neither is `ee` ... but `this` works fine 133 | console.log(this); 134 | // this log will be on the Node.js site, it won't log 135 | // anything on the browser 136 | console.log('stuff', value); 137 | }); 138 | 139 | // DOM listeners should be async if these need to signal 140 | // or interact with the foreign code because ... 141 | someButtom.addEventListener('click', async () => { 142 | // ... foreign code always need to be awaited! 143 | await ee.emit('stuff', 123); 144 | }); 145 | }); 146 | ``` 147 | 148 |
149 |
150 | 151 |
152 | Is it safe? 153 |
154 | 155 | Theoretically, this is either "_as safe as_", or "_as unsafe as_", _electron_ can be, but technically, the whole idea behind is based on client side code evaluation through a shared [vm](https://nodejs.org/api/vm.html) and always the [same context](https://nodejs.org/api/vm.html#vm_script_runincontext_contextifiedobject_options) per each client, although ensuring a "_share nothing_" `global` object per each context, so that multiple clients, with multiple instances/invokes, won't interfere with each other, given the same script on the page. 156 | 157 | If the `ELECTROFF_ONCE=1` environment variable is present, *electroff* will increase security in the following way: 158 | 159 | * a client can use *electroff* only via `import electroff from '/electroff?module'`, and any attempt to retrieve the electroff script in a different way will fail 160 | * previous point ensures that the module can be executed *only once*, so there's one single room/window in the page to define its behavior, anot nothing else can interfeer with the server side *vm* 161 | * using *CSP* would also work so that only known code on the page can safely run, and there's no `eval` nor `Function` call in here, so that nothing else can be injected 162 | 163 | Regardless of the `ELECTROFF_ONCE=1` security guard though, please **bear in mind** that even if the whole communication channel is somehow based on very hard to guess unique random _IDs_ per client, this project/module is **not suitable for websites**, but it can be used in any _IoT_ related project, kiosk, or standalone applications, where we are sure there is no malicious code running arbitrary _JS_ on our machines, which is not always the case for online Web pages. 164 | 165 |
166 |
167 | 168 |
169 | Are Node.js instances possible? 170 |
171 | 172 | Yes, but there are at least two things to keep in mind: 173 | 174 | * any _Node.js_ instance *should* be _awaited_ on creation, i.e.: `const instance = await new require('events').EventEmitter;`, unless we're waiting for a specific listener, in which case it's better to await `until(thing).is('ready')` (see next F.A.Q.) 175 | * there is currently no way to automatically free the _vm_ from previously created instances, if not by explicitly using `remove(instance)` 176 | 177 | Last point means the _vm_ memory related to any client would be freed *only* once the client refreshes the page, or closes the tab, but there's the possibility that the client crashes or has no network all of a sudden, and in such case the _vm_ will trash any reference automatically, in about 5 minutes or more. 178 | 179 |
180 |
181 | 182 |
183 | How to react to/until Node.js events? 184 |
185 | 186 | The `until` utility keeps the _POST_ request hanging *until* the observed event is triggered _once_. It pollutes the _emitter_, if not polluted already, with an `is(eventName)` that returns a promise resolved once the event name happens. 187 | 188 | Following an example of how this could work in practice. 189 | 190 | ```js 191 | CommonJS(async ({require, until}) => { 192 | const five = require('johnny-five'); 193 | 194 | // no need to await here, or ready could 195 | // be fired before the next request is performed 196 | const board = new five.Board(); 197 | 198 | // simply await everything at once in here 199 | await until(board).is('ready'); 200 | 201 | // now all board dependent instances can be awaited 202 | const led = await new five.Led(13); 203 | // so that it's possible to await each method/invoke/property 204 | await led.blink(500); 205 | 206 | document.body.textContent = `it's blinking!`; 207 | }); 208 | ``` 209 | 210 |
211 |
212 | 213 |
214 | Any best practice? 215 |
216 | 217 | At this early stage, I can recommend only few best-practices I've noticed while playing around with this module: 218 | 219 | * don't _overdo_ server side instances/references, try to reach *only* the utilities you need the most, instead of creating everything on the _vm_ side 220 | * when a server side reference *method* is invoked, you *must await* it, i.e. `await emitter.setMaxListeners(20)`. This grants next time you `await emitter.getMaxListeners()` you'll receive the answer you expect 221 | * template literals are passed as plain arrays. If your library optimizes on template literals uniqueness, it will always re-parse/re-do any dance, because the array on the server side will be always a different one. Create a file that queries the DB, and simply `require("./db-helper")` instead of writing all SQL queries on the client side, and use _Node.js_ regular helpers/files whenever it works 222 | * try to keep `global` references to a minimum amount, as the back and forward dance is quite expensive, and most of the time you won't need it 223 | * if any needed instance has an emit once ready, `const instance = new Thing; await until(instance).is('ready')` instead of `const instance = await new Thing; await instance.once('ready', doThing)`, so you ensure your instance is ready within the client side scope, instead of needing a foreign callback that cannot reach such scope 224 | 225 |
226 |
227 | 228 |
229 | What about performance? 230 |
231 | 232 | The _JS_ that runs on the browsers is as fast as it can get, but every _Node.js_ handled setter, getter, or method invoke, will pass through a _POST_ request, with some _vm_ evaluation, recursive-capable serving and parsing, and eventually a result on the client. 233 | 234 | This won't exactly be high performance but, for what I could try, performance is *good enough*, for most _IoT_ or standalone application. 235 | 236 |
237 |
238 | 239 |
240 | What kind of data can be exchanged? 241 |
242 | 243 | Any *JSON* serializable data, with the nice touch that [flatted](https://github.com/WebReflection/flatted#readme) gives to responses objects, where even circular references can be returned to the client. 244 | 245 | **However**, you cannot send circular references to the server, *but* you can send *callbacks* that will be passed along as string to evaluate, meaning any surrounding closure variable won't be accessible once on the server so ... be careful when passing callbacks around. 246 | 247 | **On Node.js side** though, be sure you use _promisify_ or any already promisified version of its API, as utilities with callbacks can't be awaited, hence will likely throw errors, unless these are needed to operate exclusively on the _Node.js_ side. 248 | 249 |
250 |
251 | 252 |
253 | How is this different from electron? 254 |
255 | 256 | _electron_ is an awesome project, and I love it with all my heart ♥ 257 | 258 | However, it has its own caveats: 259 | 260 | * _electron_ itself is a huge dependency, and there are multiple versions, where different apps might use/need different versions, so its size is exponential, and it doesn't play too well with the fast pace _Node.js_ and its modules ecosystem get updated 261 | * _electron_ uses modules that are not the same one used in _Node.js_. If we update a module in the system, _electron_ might still use its own version of such module 262 | * _electron_ doesn't work cross browser, because it brings its own browser itself. This is both great, for application reliability across platforms, and bad, for platforms where there is already a better browser, and all it's missing is the ability to seamlessly interact with the system version of _Node.js_. As example, the best browser for _IoT_ devices is [WPE WebKit](https://wpewebkit.org/), and not _Chrome/ium_, because _WPE WebKit_ offers Hardware Acceleration, with a minimal footprint, and great performance for embedded solutions 263 | * _electron_ cannot serve multiple clients, as each client would need an instance of the same _electron_ app. This module provides the ability, for any reasonably modern browser, to perform _Node.js_ operations through the Web, meaning that you don't need anyone to install _electron_, as everything is already working/available through this module to the masses 264 | 265 |
266 |
267 | 268 |
269 | Is this ready for production? 270 |
271 | 272 | This module is currently in its early development stage, and there are at least two main concerns regarding it: 273 | 274 | * the `remove(...)` utility requires user-land care, 'cause if it's not performed, the _vm_ behind the scene could retain in RAM references "_forever_", or at least up to the time the associated _UID_ to each client gets purged (once every 5 minutes) 275 | * the purge mechanism is based on requests: no requests whatsoever in 5 minutes, nothing gets purged 276 | 277 | This means we can use this project in _IoT_ or standalone projects, as long as its constrains are clear, and user being redirected to a fake 404 page that requires them to reload is acceptable. 278 | 279 |
280 |
281 | 282 |
283 | Which browser is compatible? 284 |
285 | 286 | All evergreen browsers should work just fine, but these are the requirements for this module to work on the client: 287 | 288 | * `async/await` [native capability](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function#Browser_compatibility) 289 | * `fetch` [native API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Browser_compatibility) 290 | * `navigator.sendBeacon` [native method](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon#Browser_compatibility) 291 | 292 |
293 |
294 | 295 |
296 | How to debug? 297 |
298 | 299 | If there is a `DEBUG=1` or a `DEBUG=true` environment variable, a lot of helpful details are logged in the terminal, either via `console.log`, or via `console.error`, when something has been caught as an error. 300 | 301 |
302 |
303 | 304 | 305 | ## Examples 306 | 307 | * [Raspberri Pi + oled](./examples/oled/README.md), write on the RPi oled screen from any browser 308 | --------------------------------------------------------------------------------