From a0f836e020536e1dcb96acae64714f18ca58a857 Mon Sep 17 00:00:00 2001 From: Fabien Benetou Date: Fri, 5 May 2023 09:57:08 +0200 Subject: [PATCH] config file for local services, auth --- index.js | 227 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 164 insertions(+), 63 deletions(-) diff --git a/index.js b/index.js index 1e6f189..61f25bc 100644 --- a/index.js +++ b/index.js @@ -7,22 +7,49 @@ const express = require("express"); // could be good to replace with c // Get port or default to 8082 const port = process.env.PORT || 8082; +const protocol = 'https' +const subclass = '192.168.4.' // Setup and configure Express http server. const app = express(); app.use(express.static(path.resolve(__dirname, ".", "examples"))); +process.title = 'offtopus' // offline-octopus seems too long + +const filename = 'editor.html' +const fileSaveFullPath = path.join(__dirname,'examples', filename) +const minfilename = 'editor.html.minimal' +const minfileSaveFullPath = path.join(__dirname,'examples', minfilename) + +const sshconfigpath = path.resolve(process.env.HOME,'.ssh','config') +const propath = path.resolve(process.env.HOME,'Prototypes') + +// does not apply in a P2P fashion, must rely on local configuration here config.json +let localServices = [ ] +const configFilePath = path.resolve(__dirname, "config.json") +if (fs.existsSync( configFilePath ) ){ + const configurationFromFile = JSON.parse( fs.readFileSync( configFilePath ).toString() ) + localServices = configurationFromFile +} +console.log(localServices) + +const utilsCmd = { // security risk but for now not accepting user input so safer + 'update' : { desc: 'note that will lose the state, e.g foundpeers', + cmd: 'killall '+process.title+' && ' }, + // should first download the new version and proceed only if new + // ideally all handles within node + 'shutdown' : { cmd: 'shutdown -h now' }, // not available everywhere, e.g unrooted Quest + 'listprototypes': { cmd: 'ls', context: {cwd: propath}, + format: res => res.toString().split('\n') + }, +} + const instructions = ` /home/deck/.ssh/ trusted context, i.e on closed WiFi and over https, still. could check as bearer /home/deck/.ssh/config could limit to known IP range or class e.g cat .ssh/config | grep 192.168.4. -C 3 could also re-add new entries rather than extend the format -/home/deck/.ssh/authorized_keys -/home/deck/.ssh/known_hosts -/home/deck/.ssh/id_rsa_steamdeck -/home/deck/.ssh/id_rsa_steamdeck.pub - /home/deck/server.locatedb seems to be plain text with metadata /home/deck/desktop.plocate.db @@ -31,6 +58,7 @@ const instructions = ` ssh remarkable2 to get drawing preview conversion should be done, if necessary, on device + not feasible right now without toltec to get opkg to get node for typed text cat 43*.rm | sed "s/[^a-zA-z0-9 ]//g" | sed "s/[OT,EC]//g" @@ -38,6 +66,20 @@ util functions modify WiFi parameters, including AP if available shutdown/reboot ` + +const auth_instructions = `generate md5 from pub offline-octopus then provide as bearer query param` + +app.get('/', (req, res) => { + res.send( instructions ) +}) + +app.get('/authtest', (req, res) => { + if (req.query?.bearer != md5fromPub){ res.send( auth_instructions); return; } + // relying on this line for each specific route + // this is NOT authentification proper, even less secure + // this is done ONLY to avoid mistakes on a secure LAN + res.json( {msg: "success"} ) +}) app.get('/pwa', (req, res) => { // for offline use on mobile or VR headset // should try to sync back when devices connect back @@ -45,54 +87,91 @@ app.get('/pwa', (req, res) => { // can be cascading or federatede or properly P2P // responsive programming also, not "just" design res.redirect('pwa/index.html') + // see also /editor }) -const utilsCmd = { - 'shutdown' : { cmd: 'shutdown -h now' }, - 'listprototypes': { cmd: 'ls', context: {cwd: '/home/deck/Prototypes'}, - // more reliably path.join(__dirname,'') - format: res => res.toString().split('\n') - }, -} -app.get('/exec', (req, res) => { + +let resultFromLastGlobalCmd = {} + +// should check on e.g publicKey or md5fromPub +app.get('/allpeers/exec', (req, res) => { + if (req.query?.bearer != md5fromPub){ res.send( auth_instructions); return; } if (!req.query.cmdName){ res.json(utilsCmd) } else { + foundPeers.map( i => { + let url=protocol+'://'+subclass+i+':'+port + +'/exec?cmdName='+req.query.cmdName + +'&bearer='+req.query.bearer + let opt={rejectUnauthorized: false} + https.get(url, opt, res => { + let data = ''; + res.on('data', chunk => { + data += chunk; + }); + res.on('end', () => { + console.log('data', url, data) + data = JSON.parse(data); + resultFromLastGlobalCmd[url] = data + }) + }).on('error', err => { + console.log(err.message); + }).end() + }) + res.json( {msg: 'started'} ) // could redirect('/all/result') too after a timeout + } +}) +app.get('/allpeers/result', (req, res) => { + res.json( resultFromLastGlobalCmd ) +}) - let resultFromExecution = execSync(utilsCmd[req.query.cmdName].cmd, utilsCmd[req.query.cmdName].context) - let formatter = utilsCmd[req.query.cmdName].format - if (formatter) resultFromExecution = formatter(resultFromExecution) - res.json( resultFromExecution ) +app.get('/exec', (req, res) => { + if (req.query?.bearer != md5fromPub){ res.send( auth_instructions); return; } + if (!req.query.cmdName){ + res.json(utilsCmd) + } else { + res.json( execConfiguredCommand(req.query.cmdName) ) } }) + +function execConfiguredCommand(cmdName){ + let resultFromExecution = execSync(utilsCmd[cmdName].cmd, utilsCmd[cmdName].context) + let formatter = utilsCmd[cmdName].format + if (formatter) resultFromExecution = formatter(resultFromExecution) + return resultFromExecution +} + // app.get('/interface/unregister', (req, res) => { // app.get('/interface/register', (req, res) => { // could be use for e.g reMarkable2, Quest2, etc with specifically accepted or prefered formats + app.get('/services/unregister', (req, res) => { res.json( {msg: 'not yet implemented'}) }) + app.get('/services/register', (req, res) => { res.json( {msg: 'not yet implemented'}) // example {name:'hmdlink', desc:'share URL between local devices', protocol:'http', port:8082, path: '/hmdlink', url:'http://192.168.4.3:8082/hmdlink'}, }) + app.get('/updates', (req, res) => { // could rely on a git reachable by all peers // this might be feasible from this very https server as read-only + // surely feasible via ssh + // could killall offtopus first then pull then restart detached res.json( {msg: 'not yet implemented'}) }) + app.get('/recentfiles', (req, res) => { // e.g lsof | grep home | grep vim | grep -v firefox // or history | grep vi // should be available after (ideally local) conversion if needed, e.g rm -> .svg on reMarkable res.json( {msg: 'not yet implemented'}) }) + app.get('/services', (req, res) => { // could be probbed first to check for availability // should be updated also via register/unregister - res.json( [ - {name:'kiwix', desc:'offline Wikipedia, Stackoverflow, etc', protocol:'http', port:8080, url:'http://192.168.4.3:8080'}, - {name:'hmdlink', desc:'share URL between local devices', protocol:'http', port:8082, path: '/hmdlink', url:'http://192.168.4.3:8082/hmdlink'}, - - ] ) + res.json( localServices ) }) let dynURL = 'https://192.168.4.1/offline.html' @@ -101,25 +180,34 @@ app.get('/hmdlink/set', (req, res) => { // could be a PUT instead dynURL = req.query.url res.redirect( dynURL ) }) -app.get('/webxr', (req, res) => { - res.redirect( '/local-metaverse-tooling/local-aframe-test.html' ) -}) + app.get('/hmdlink', (req, res) => { res.redirect( dynURL ) }) -app.get('/json', (req, res) => { - res.json( instructions.split('\n') ) + +app.get('/webxr', (req, res) => { + res.redirect( '/local-metaverse-tooling/local-aframe-test.html' ) }) + +// user for /scan to populate foundPeers app.get('/available', (req, res) => { res.json( true ) }) + app.get('/foundpeers', (req, res) => { res.json( foundPeers ) }) + let foundPeers = [] app.get('/scan', (req, res) => { + scanpeers() + res.json( {msg: 'started'} ) // could redirect('/foundpeers') too after a timeout +}) + +function scanpeers(){ + foundPeers = [] for (let i=1;i<25;i++){ // async so blasting, gives very quick result for positives - let url='https://192.168.4.'+i+':8082/available' + let url=protocol+'://'+subclass+i+':'+port+'/available' let opt={rejectUnauthorized: false} https.get(url, opt, res => { let data = ''; @@ -135,20 +223,60 @@ app.get('/scan', (req, res) => { //console.log(err.message); usually ECONNREFUSED or EHOSTUNREACH }).end() } - res.json( {msg: 'started'} ) // could redirect('/foundpeers') too -}) -app.get('/', (req, res) => { - res.send( instructions ) +} + +app.get('/sshconfig', (req, res) => { + res.json( getSshConfig() ) + // should filter on foundPeers to avoid offline peers }) + +// returns connections string for e.g sshfs +function getSshConfig(){ + // here assume user deck + let txt = fs.readFileSync(sshconfigpath).toString() + return txt.split('Host ') + .filter( m => m.match(subclass) ) + .map( c => { + p = c.replaceAll(' ','') + .split('\n') + .filter(i=>i.match(/^[a-zA-Z]/)); + return p.filter(pm=>pm.match('User '))[0].replace('User ','')+ + p.filter(pm=>pm.match('HostName'))[0].replace('HostName ','@') + }) +} + +/* + e.g AFTER mounting + f.readdirSync('./sshfsmounts/').map( d=>f.readdirSync('./sshfsmounts/'+d) ) + location : /home/deck/Prototypes/offline-octopus/sshfsmounts + sshfs remarkable2:/home/root/xochitl-data/ remarkable2/ + works if available + sshfs fabien@192.168.4.1:/home/fabien/ rpi0/ + still prompts for password, need manual login + ls rpi0/ + ls remarkable2/ + + must be done after passwordless login, i.e after ssh-copy-id + made a dedicated key : /home/deck/.ssh/id_rsa_offlineoctopus + easier to revoke if need be + */ + +const publicKeyPath = path.resolve(process.env.HOME,'.ssh','id_rsa_offlineoctopus.pub') +const publicKey = fs.readFileSync(publicKeyPath).toString().split(' ')[1] + +const md5fromPub = fs.readFileSync( path.resolve(__dirname, ".keyfrommd5")).toString().replace('\n','') + app.get('/localprototypes', (req, res) => { // res.json( spawn('find Prototypes/ -iwholename */.git/config | xargs grep git.benetou.fr') ) // should use execSync now }) + app.get('/editor/recover', (req, res) => { // could move the previous file with time stamp fs.copyFileSync(minfileSaveFullPath, fileSaveFullPath ) res.json( {msg: 'copied'} ) }) + /* * example of finding past local solution, here shiki for syntax highlighting * @@ -160,14 +288,13 @@ onig.wasm see syntax-highlighting branch in SpaSca git repository in /home/deck/serverhome/fabien/web/future_of_text_demo/engine/ */ -let filename = 'editor.html' -let fileSaveFullPath = path.join(__dirname,'examples', filename) -let minfilename = 'editor.html.minimal' -let minfileSaveFullPath = path.join(__dirname,'examples', minfilename) + + app.get('/editor/read', (req, res) => { content = fs.readFileSync(fileSaveFullPath ).toString() res.json( {msg: content} ) }) + app.get('/editor/save', (req, res) => { let content = req.query.content // does not escape, loses newlines if (!content){ @@ -188,32 +315,6 @@ const webServer = https.createServer(credentials, app); // const webServer = http.createServer(app); // Listen on port webServer.listen(port, () => { - console.log("listening on http://localhost:" + port); + console.log("listening on "+protocol+"://localhost:" + port); }); -// returns connections string for e.g sshfs -function loadSshConfig(){ - // here assume user deck - let txt = f.readFileSync('/home/deck/.ssh/config').toString() - return txt.split('Host ') - .filter( m => m.match('192.168.4.') ) - .map( c => { - p = c.replaceAll(' ','') - .split('\n') - .filter(i=>i.match(/^[a-zA-Z]/)); - return p.filter(pm=>pm.match('User '))[0].replace('User ','')+ - p.filter(pm=>pm.match('HostName'))[0].replace('HostName ','@') - }) -} -/* - e.g AFTER mounting - f.readdirSync('./sshfsmounts/').map( d=>f.readdirSync('./sshfsmounts/'+d) ) - location : /home/deck/Prototypes/offline-octopus/sshfsmounts - sshfs remarkable2:/home/root/xochitl-data/ remarkable2/ - works if available - sshfs fabien@192.168.4.1:/home/fabien/ rpi0/ - still prompts for password, need manual login - ls rpi0/ - ls remarkable2/ - */ -