config file for local services, auth
This commit is contained in:
229
index.js
229
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 {
|
||||
|
||||
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 )
|
||||
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( [
|
||||
{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/
|
||||
*/
|
||||
|
||||
|
||||
Reference in New Issue
Block a user