const fs = require("fs"); const http = require("http"); const https = require("https"); const path = require("path"); const {execSync} = require('child_process'); const express = require("express"); // could be good to replace with code, no dep // express does not seem architecture dependant thus copying node_modules sufficed for now // (worked at least on RPi, Deck, Quest and x64 machines) // 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 // e.g git clone deck@localhost:~/Prototypes/offline-octopus/ // should see /sshconfig // 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/server.locatedb seems to be plain text with metadata /home/deck/desktop.plocate.db seems to be a specific format, binary or maybe compressed both should be queriable via http with json output 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" 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 // same when itself connects back to (Internet) own server e.g benetou.fr // can be cascading or federatede or properly P2P // responsive programming also, not "just" design res.redirect('pwa/index.html') // see also /editor }) 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 ) }) 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( localServices ) }) let dynURL = 'https://192.168.4.1/offline.html' app.get('/hmdlink/set', (req, res) => { // could be a PUT instead // e.g http://192.168.4.3:8082/hmdlink/set?url=http://192.168.4.3:8082/114df5f8-3921-42f0-81e7-48731b563571.thumbnails/f07120ba-0ca1-429d-869f-c704a52b7aa3.png dynURL = req.query.url res.redirect( dynURL ) }) app.get('/hmdlink', (req, res) => { res.redirect( dynURL ) }) 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=protocol+'://'+subclass+i+':'+port+'/available' let opt={rejectUnauthorized: false} https.get(url, opt, res => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { data = JSON.parse(data); foundPeers.push(i) // could also register there and then }) }).on('error', err => { //console.log(err.message); usually ECONNREFUSED or EHOSTUNREACH }).end() } } 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 * (deck@steamdeck serverhome)$ find . -iname shiki ./fabien/web/future_of_text_demo/content/shiki (deck@steamdeck serverhome)$ ls ./fabien/web/future_of_text_demo/content/shiki/dist/ onig.wasm see syntax-highlighting branch in SpaSca git repository in /home/deck/serverhome/fabien/web/future_of_text_demo/engine/ */ 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){ res.json( {msg: 'missing content'} ) } else { console.log('writting', content) fs.writeFileSync(fileSaveFullPath, content) res.json( {msg: 'written to '+fileSaveFullPath} ) } }) const privateKey = fs.readFileSync("naf-key.pem", "utf8"); const certificate = fs.readFileSync("naf.pem", "utf8"); const credentials = { key: privateKey, cert: certificate }; const webServer = https.createServer(credentials, app); // Start Express http server // const webServer = http.createServer(app); // Listen on port webServer.listen(port, () => { console.log("listening on "+protocol+"://localhost:" + port); });