config file for local services, auth

reverse-proxy
Fabien Benetou 2 years ago
parent a62cddd437
commit a0f836e020
  1. 227
      index.js

@ -7,22 +7,49 @@ const express = require("express"); // could be good to replace with c
// Get port or default to 8082 // Get port or default to 8082
const port = process.env.PORT || 8082; const port = process.env.PORT || 8082;
const protocol = 'https'
const subclass = '192.168.4.'
// Setup and configure Express http server. // Setup and configure Express http server.
const app = express(); const app = express();
app.use(express.static(path.resolve(__dirname, ".", "examples"))); 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 = ` const instructions = `
/home/deck/.ssh/ /home/deck/.ssh/
trusted context, i.e on closed WiFi and over https, still. could check as bearer trusted context, i.e on closed WiFi and over https, still. could check as bearer
/home/deck/.ssh/config /home/deck/.ssh/config
could limit to known IP range or class e.g cat .ssh/config | grep 192.168.4. -C 3 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 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 /home/deck/server.locatedb
seems to be plain text with metadata seems to be plain text with metadata
/home/deck/desktop.plocate.db /home/deck/desktop.plocate.db
@ -31,6 +58,7 @@ const instructions = `
ssh remarkable2 to get drawing preview ssh remarkable2 to get drawing preview
conversion should be done, if necessary, on device conversion should be done, if necessary, on device
not feasible right now without toltec to get opkg to get node
for typed text for typed text
cat 43*.rm | sed "s/[^a-zA-z0-9 ]//g" | sed "s/[OT,EC]//g" 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 modify WiFi parameters, including AP if available
shutdown/reboot 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) => { app.get('/pwa', (req, res) => {
// for offline use on mobile or VR headset // for offline use on mobile or VR headset
// should try to sync back when devices connect back // 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 // can be cascading or federatede or properly P2P
// responsive programming also, not "just" design // responsive programming also, not "just" design
res.redirect('pwa/index.html') res.redirect('pwa/index.html')
// see also /editor
}) })
const utilsCmd = {
'shutdown' : { cmd: 'shutdown -h now' }, let resultFromLastGlobalCmd = {}
'listprototypes': { cmd: 'ls', context: {cwd: '/home/deck/Prototypes'},
// more reliably path.join(__dirname,'') // should check on e.g publicKey or md5fromPub
format: res => res.toString().split('\n') app.get('/allpeers/exec', (req, res) => {
}, if (req.query?.bearer != md5fromPub){ res.send( auth_instructions); return; }
}
app.get('/exec', (req, res) => {
if (!req.query.cmdName){ if (!req.query.cmdName){
res.json(utilsCmd) res.json(utilsCmd)
} else { } 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) app.get('/exec', (req, res) => {
let formatter = utilsCmd[req.query.cmdName].format if (req.query?.bearer != md5fromPub){ res.send( auth_instructions); return; }
if (formatter) resultFromExecution = formatter(resultFromExecution) if (!req.query.cmdName){
res.json( resultFromExecution ) 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/unregister', (req, res) => {
// app.get('/interface/register', (req, res) => { // app.get('/interface/register', (req, res) => {
// could be use for e.g reMarkable2, Quest2, etc with specifically accepted or prefered formats // could be use for e.g reMarkable2, Quest2, etc with specifically accepted or prefered formats
app.get('/services/unregister', (req, res) => { app.get('/services/unregister', (req, res) => {
res.json( {msg: 'not yet implemented'}) res.json( {msg: 'not yet implemented'})
}) })
app.get('/services/register', (req, res) => { app.get('/services/register', (req, res) => {
res.json( {msg: 'not yet implemented'}) 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'}, // 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) => { app.get('/updates', (req, res) => {
// could rely on a git reachable by all peers // could rely on a git reachable by all peers
// this might be feasible from this very https server as read-only // 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'}) res.json( {msg: 'not yet implemented'})
}) })
app.get('/recentfiles', (req, res) => { app.get('/recentfiles', (req, res) => {
// e.g lsof | grep home | grep vim | grep -v firefox // e.g lsof | grep home | grep vim | grep -v firefox
// or history | grep vi // or history | grep vi
// should be available after (ideally local) conversion if needed, e.g rm -> .svg on reMarkable // should be available after (ideally local) conversion if needed, e.g rm -> .svg on reMarkable
res.json( {msg: 'not yet implemented'}) res.json( {msg: 'not yet implemented'})
}) })
app.get('/services', (req, res) => { app.get('/services', (req, res) => {
// could be probbed first to check for availability // could be probbed first to check for availability
// should be updated also via register/unregister // should be updated also via register/unregister
res.json( [ res.json( localServices )
{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'},
] )
}) })
let dynURL = 'https://192.168.4.1/offline.html' 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 dynURL = req.query.url
res.redirect( dynURL ) res.redirect( dynURL )
}) })
app.get('/webxr', (req, res) => {
res.redirect( '/local-metaverse-tooling/local-aframe-test.html' )
})
app.get('/hmdlink', (req, res) => { app.get('/hmdlink', (req, res) => {
res.redirect( dynURL ) 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) => { app.get('/available', (req, res) => {
res.json( true ) res.json( true )
}) })
app.get('/foundpeers', (req, res) => { app.get('/foundpeers', (req, res) => {
res.json( foundPeers ) res.json( foundPeers )
}) })
let foundPeers = [] let foundPeers = []
app.get('/scan', (req, res) => { 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 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} let opt={rejectUnauthorized: false}
https.get(url, opt, res => { https.get(url, opt, res => {
let data = ''; let data = '';
@ -135,20 +223,60 @@ app.get('/scan', (req, res) => {
//console.log(err.message); usually ECONNREFUSED or EHOSTUNREACH //console.log(err.message); usually ECONNREFUSED or EHOSTUNREACH
}).end() }).end()
} }
res.json( {msg: 'started'} ) // could redirect('/foundpeers') too }
})
app.get('/', (req, res) => { app.get('/sshconfig', (req, res) => {
res.send( instructions ) 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) => { app.get('/localprototypes', (req, res) => {
// res.json( spawn('find Prototypes/ -iwholename */.git/config | xargs grep git.benetou.fr') ) // res.json( spawn('find Prototypes/ -iwholename */.git/config | xargs grep git.benetou.fr') )
// should use execSync now // should use execSync now
}) })
app.get('/editor/recover', (req, res) => { app.get('/editor/recover', (req, res) => {
// could move the previous file with time stamp // could move the previous file with time stamp
fs.copyFileSync(minfileSaveFullPath, fileSaveFullPath ) fs.copyFileSync(minfileSaveFullPath, fileSaveFullPath )
res.json( {msg: 'copied'} ) res.json( {msg: 'copied'} )
}) })
/* /*
* example of finding past local solution, here shiki for syntax highlighting * 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 see syntax-highlighting branch in SpaSca git repository
in /home/deck/serverhome/fabien/web/future_of_text_demo/engine/ 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) => { app.get('/editor/read', (req, res) => {
content = fs.readFileSync(fileSaveFullPath ).toString() content = fs.readFileSync(fileSaveFullPath ).toString()
res.json( {msg: content} ) res.json( {msg: content} )
}) })
app.get('/editor/save', (req, res) => { app.get('/editor/save', (req, res) => {
let content = req.query.content // does not escape, loses newlines let content = req.query.content // does not escape, loses newlines
if (!content){ if (!content){
@ -188,32 +315,6 @@ const webServer = https.createServer(credentials, app);
// const webServer = http.createServer(app); // const webServer = http.createServer(app);
// Listen on port // Listen on port
webServer.listen(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/
*/

Loading…
Cancel
Save